initial commit
[squeep-indie-auther] / src / db / schema-version-helper.js
1 'use strict';
2
3 const fs = require('fs');
4 const path = require('path');
5
6 /**
7 * Utility functions for wrangling schema migrations.
8 * This mostly just deals with sorting and comparing 'x.y.z' version
9 * strings, with some presumptions about directory layouts and whatnot.
10 */
11
12 /**
13 * @typedef {Object} SchemaVersionObject
14 * @property {Number} major
15 * @property {Number} minor
16 * @property {Number} patch
17 */
18
19
20 /**
21 * Split a dotted version string into parts.
22 * @param {String} v
23 * @returns {SchemaVersionObject}
24 */
25 function schemaVersionStringToObject(v) {
26 const [ major, minor, patch ] = v.split('.', 3).map((x) => parseInt(x, 10));
27 return { major, minor, patch };
28 }
29
30
31 /**
32 * Render a version object numerically.
33 * @param {SchemaVersionObject} v
34 * @returns {Number}
35 */
36 function schemaVersionObjectToNumber(v) {
37 const vScale = 1000;
38 return parseInt(v.major) * vScale * vScale + parseInt(v.minor) * vScale + parseInt(v.patch);
39 }
40
41
42 /**
43 * Convert dotted version string into number.
44 * @param {String} v
45 * @returns {Number}
46 */
47 function schemaVersionStringToNumber(v) {
48 return schemaVersionObjectToNumber(schemaVersionStringToObject(v));
49 }
50
51
52 /**
53 * Version string comparison, for sorting.
54 * @param {String} a
55 * @param {String} b
56 * @returns {Number}
57 */
58 function schemaVersionStringCmp(a, b) {
59 return schemaVersionStringToNumber(a) - schemaVersionStringToNumber(b);
60 }
61
62
63 /**
64 * Check if an entry in a directory is a directory containing a migration file.
65 * @param {String} schemaDir
66 * @param {String} name
67 * @returns {Boolean}
68 */
69 function isSchemaMigrationDirectory(schemaDir, name, migrationFile = 'apply.sql') {
70 // eslint-disable-next-line security/detect-non-literal-fs-filename
71 const nameStat = fs.statSync(path.join(schemaDir, name));
72 if (nameStat.isDirectory()) {
73 let applyStat;
74 try {
75 // eslint-disable-next-line security/detect-non-literal-fs-filename
76 applyStat = fs.statSync(path.join(schemaDir, name, migrationFile));
77 return applyStat.isFile();
78 } catch (e) {
79 return false;
80 }
81 }
82 return false;
83 }
84
85
86 /**
87 * Return an array of schema migration directory names within engineDir,
88 * sorted in increasing order.
89 * @param {String} engineDir
90 * @returns {String[]}
91 */
92 function allSchemaVersions(engineDir) {
93 const schemaDir = path.join(engineDir, 'sql', 'schema');
94 // eslint-disable-next-line security/detect-non-literal-fs-filename
95 const availableVersions = fs.readdirSync(schemaDir).filter((d) => isSchemaMigrationDirectory(schemaDir, d));
96 availableVersions.sort(schemaVersionStringCmp);
97 return availableVersions;
98 }
99
100
101 /**
102 * Return an array of schema migration directory names within engineDir,
103 * which are within supported range, and are greater than the current
104 * @param {String} engineDir
105 * @param {SchemaVersionObject} current
106 * @param {Object} supported
107 * @param {SchemaVersionObject} supported.min
108 * @param {SchemaVersionObject} supported.max
109 * @returns {String[]}
110 */
111 function unappliedSchemaVersions(engineDir, current, supported) {
112 const min = schemaVersionObjectToNumber(supported.min);
113 const max = schemaVersionObjectToNumber(supported.max);
114 const cur = schemaVersionObjectToNumber(current);
115 const available = allSchemaVersions(engineDir);
116 return available.filter((a) => {
117 a = schemaVersionStringToNumber(a);
118 return a >= min && a <= max && a > cur;
119 });
120 }
121
122
123 module.exports = {
124 schemaVersionStringToObject,
125 schemaVersionObjectToNumber,
126 schemaVersionStringToNumber,
127 schemaVersionStringCmp,
128 isSchemaMigrationDirectory,
129 allSchemaVersions,
130 unappliedSchemaVersions,
131 };