spooder 4.5.7 → 4.5.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -1
- package/bun.lock +51 -0
- package/package.json +1 -1
- package/src/api.ts +95 -23
package/README.md
CHANGED
|
@@ -1215,6 +1215,16 @@ await parse_template('Hello {$world}', replacer);
|
|
|
1215
1215
|
</html>
|
|
1216
1216
|
```
|
|
1217
1217
|
|
|
1218
|
+
`parse_template` supports optional scopes with the following syntax.
|
|
1219
|
+
|
|
1220
|
+
```html
|
|
1221
|
+
{$if:foo}I love {$foo}{/if}
|
|
1222
|
+
```
|
|
1223
|
+
Contents contained inside an `if` block will be rendered providing the given value, in this case `foo` is truthy in the substitution table.
|
|
1224
|
+
|
|
1225
|
+
An `if` block is only removed if `drop_missing` is `true`, allowing them to persist through multiple passes of a template.
|
|
1226
|
+
|
|
1227
|
+
|
|
1218
1228
|
`parse_template` supports looping arrays with the following syntax.
|
|
1219
1229
|
|
|
1220
1230
|
```html
|
|
@@ -1540,7 +1550,28 @@ Each revision should be clearly marked with a comment containing the revision nu
|
|
|
1540
1550
|
|
|
1541
1551
|
Everything following a revision header is considered part of that revision until the next revision header or the end of the file, allowing for multiple SQL statements to be included in a single revision.
|
|
1542
1552
|
|
|
1543
|
-
When calling `db_update_schema_sqlite`, unapplied revisions will be applied in ascending order (regardless of order within the file) until the schema is up-to-date.
|
|
1553
|
+
When calling `db_update_schema_sqlite`, unapplied revisions will be applied in ascending order (regardless of order within the file) until the schema is up-to-date.
|
|
1554
|
+
|
|
1555
|
+
It is acceptable to omit keys. This can be useful to prevent repitition when managing stored procedures, views or functions.
|
|
1556
|
+
|
|
1557
|
+
```sql
|
|
1558
|
+
-- example of repetitive declaration
|
|
1559
|
+
|
|
1560
|
+
-- [1] create view
|
|
1561
|
+
CREATE VIEW `view_test` AS SELECT * FROM `table_a` WHERE col = 'foo';
|
|
1562
|
+
|
|
1563
|
+
-- [2] change view
|
|
1564
|
+
DROP VIEW IF EXISTS `view_test`;
|
|
1565
|
+
CREATE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
|
|
1566
|
+
```
|
|
1567
|
+
Instead of unnecessarily including each full revision of a procedure, view or function in the schema file, simply store the most up-to-date one and increment the version.
|
|
1568
|
+
```sql
|
|
1569
|
+
-- [2] create view
|
|
1570
|
+
CREATE OR REPLACE VIEW `view_test` AS SELECT * FROM `table_b` WHERE col = 'foo';
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
Schema revisions are tracked in a table called `db_schema` which is created automatically if it does not exist with the following schema.
|
|
1544
1575
|
|
|
1545
1576
|
```sql
|
|
1546
1577
|
CREATE TABLE db_schema (
|
|
@@ -1549,6 +1580,12 @@ CREATE TABLE db_schema (
|
|
|
1549
1580
|
);
|
|
1550
1581
|
```
|
|
1551
1582
|
|
|
1583
|
+
The table used for schema tracking can be changed if necessary by providing an alternative table name as the third paramater.
|
|
1584
|
+
|
|
1585
|
+
```ts
|
|
1586
|
+
await db_update_schema_sqlite(db, './schema', 'my_schema_table');
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1552
1589
|
>[!IMPORTANT]
|
|
1553
1590
|
> The entire process is transactional. If an error occurs during the application of **any** revision for **any** table, the entire process will be rolled back and the database will be left in the state it was before the update was attempted.
|
|
1554
1591
|
|
|
@@ -1565,6 +1602,20 @@ try {
|
|
|
1565
1602
|
}
|
|
1566
1603
|
```
|
|
1567
1604
|
|
|
1605
|
+
### Schema Dependencies
|
|
1606
|
+
By default, schema files are executed in the order they are provided by the operating system (generally alphabetically).
|
|
1607
|
+
|
|
1608
|
+
If you have a schema file that depends on one or more other schema files to be executed before it (for example, using foreign keys), you can specify dependencies.
|
|
1609
|
+
|
|
1610
|
+
```sql
|
|
1611
|
+
-- [deps] table_b_schema.sql, table_c_schema.sql
|
|
1612
|
+
-- [1] create table_a
|
|
1613
|
+
CREATE ...
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
>[!IMPORTANT]
|
|
1617
|
+
> Cyclic or missing dependencies will throw an error.
|
|
1618
|
+
|
|
1568
1619
|
## Legal
|
|
1569
1620
|
This software is provided as-is with no warranty or guarantee. The authors of this project are not responsible or liable for any problems caused by using this software or any part thereof. Use of this software does not entitle you to any support or assistance from the authors of this project.
|
|
1570
1621
|
|
package/bun.lock
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "spooder",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bun": "^1.1.8",
|
|
8
|
+
},
|
|
9
|
+
"optionalDependencies": {
|
|
10
|
+
"mysql2": "^3.11.0",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
"packages": {
|
|
15
|
+
"@types/bun": ["@types/bun@1.2.0", "", { "dependencies": { "bun-types": "1.2.0" } }, "sha512-5N1JqdahfpBlAv4wy6svEYcd/YfO2GNrbL95JOmFx8nkE6dbK4R0oSE5SpBA4vBRqgrOUAXF8Dpiz+gi7r80SA=="],
|
|
16
|
+
|
|
17
|
+
"@types/node": ["@types/node@22.10.9", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw=="],
|
|
18
|
+
|
|
19
|
+
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
|
20
|
+
|
|
21
|
+
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
|
22
|
+
|
|
23
|
+
"bun-types": ["bun-types@1.2.0", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-KEaJxyZfbV/c4eyG0vyehDpYmBGreNiQbZIqvVHJwZ4BmeuWlNZ7EAzMN2Zcd7ailmS/tGVW0BgYbGf+lGEpWw=="],
|
|
24
|
+
|
|
25
|
+
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
|
26
|
+
|
|
27
|
+
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
|
28
|
+
|
|
29
|
+
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
|
30
|
+
|
|
31
|
+
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
|
32
|
+
|
|
33
|
+
"long": ["long@5.2.4", "", {}, "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg=="],
|
|
34
|
+
|
|
35
|
+
"lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="],
|
|
36
|
+
|
|
37
|
+
"lru.min": ["lru.min@1.1.1", "", {}, "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q=="],
|
|
38
|
+
|
|
39
|
+
"mysql2": ["mysql2@3.12.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.6.3", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw=="],
|
|
40
|
+
|
|
41
|
+
"named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
|
|
42
|
+
|
|
43
|
+
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
|
44
|
+
|
|
45
|
+
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
|
46
|
+
|
|
47
|
+
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
|
48
|
+
|
|
49
|
+
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -173,6 +173,25 @@ export async function parse_template(template: string, replacements: Replacement
|
|
|
173
173
|
}
|
|
174
174
|
i += loop_content.length + 6;
|
|
175
175
|
}
|
|
176
|
+
} else if (buffer.startsWith('if:')) {
|
|
177
|
+
const if_key = buffer.substring(3);
|
|
178
|
+
const if_content_start_index = i + 1;
|
|
179
|
+
const if_close_index = template.indexOf('{/if}', if_content_start_index);
|
|
180
|
+
|
|
181
|
+
if (if_close_index === -1) {
|
|
182
|
+
if (!drop_missing)
|
|
183
|
+
result += '{$' + buffer + '}';
|
|
184
|
+
} else {
|
|
185
|
+
const if_content = template.substring(if_content_start_index, if_close_index);
|
|
186
|
+
const condition_value = is_replacer_fn ? await replacements(if_key) : replacements[if_key];
|
|
187
|
+
|
|
188
|
+
if (!drop_missing) {
|
|
189
|
+
result += '{$' + buffer + '}' + if_content + '{/if}';
|
|
190
|
+
} else if (condition_value) {
|
|
191
|
+
result += await parse_template(if_content, replacements, drop_missing);
|
|
192
|
+
}
|
|
193
|
+
i += if_content.length + 5;
|
|
194
|
+
}
|
|
176
195
|
} else {
|
|
177
196
|
const replacement = is_replacer_fn ? await replacements(buffer) : replacements[buffer];
|
|
178
197
|
if (replacement !== undefined)
|
|
@@ -227,6 +246,46 @@ export async function generate_hash_subs(length = 7, prefix = 'hash=', hashes?:
|
|
|
227
246
|
return hash_map;
|
|
228
247
|
}
|
|
229
248
|
|
|
249
|
+
interface DependencyTarget {
|
|
250
|
+
file_name: string;
|
|
251
|
+
deps: string[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function order_schema_dep_tree<T extends DependencyTarget>(deps: T[]): T[] {
|
|
255
|
+
const visited = new Set<string>();
|
|
256
|
+
const temp = new Set<string>();
|
|
257
|
+
const result: T[] = [];
|
|
258
|
+
const map = new Map(deps.map(d => [d.file_name, d]));
|
|
259
|
+
|
|
260
|
+
function visit(node: T): void {
|
|
261
|
+
if (temp.has(node.file_name))
|
|
262
|
+
throw new Error(`Cyclic dependency {${node.file_name}}`);
|
|
263
|
+
|
|
264
|
+
if (visited.has(node.file_name))
|
|
265
|
+
return;
|
|
266
|
+
|
|
267
|
+
temp.add(node.file_name);
|
|
268
|
+
|
|
269
|
+
for (const dep of node.deps) {
|
|
270
|
+
const dep_node = map.get(dep);
|
|
271
|
+
if (!dep_node)
|
|
272
|
+
throw new Error(`Missing dependency {${dep}}`);
|
|
273
|
+
|
|
274
|
+
visit(dep_node as T);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
temp.delete(node.file_name);
|
|
278
|
+
visited.add(node.file_name);
|
|
279
|
+
result.push(node);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
for (const dep of deps)
|
|
283
|
+
if (!visited.has(dep.file_name))
|
|
284
|
+
visit(dep);
|
|
285
|
+
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
230
289
|
type Row_DBSchema = { db_schema_table_name: string, db_schema_version: number };
|
|
231
290
|
type SchemaVersionMap = Map<string, number>;
|
|
232
291
|
|
|
@@ -249,23 +308,31 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
|
|
|
249
308
|
const schema_path = path.join(schema_file_ent.parentPath, schema_file);
|
|
250
309
|
const schema = await fs.readFile(schema_path, 'utf8');
|
|
251
310
|
|
|
311
|
+
const deps = new Array<string>();
|
|
312
|
+
|
|
252
313
|
const revisions = new Map();
|
|
253
314
|
let current_rev_id = 0;
|
|
254
315
|
let current_rev = '';
|
|
255
316
|
|
|
256
317
|
for (const line of schema.split(/\r?\n/)) {
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
318
|
+
const line_identifier = line.match(/^--\s*\[(\d+|deps)\]/);
|
|
319
|
+
if (line_identifier !== null) {
|
|
320
|
+
if (line_identifier[1] === 'deps') {
|
|
321
|
+
// Line contains schema dependencies, example: -- [deps] schema_b.sql,schema_c.sql
|
|
322
|
+
const deps_raw = line.substring(line.indexOf('deps') + 4);
|
|
323
|
+
deps.push(...deps_raw.split(',').map(e => e.trim().toLowerCase()));
|
|
324
|
+
} else {
|
|
325
|
+
// New chunk definition detected, store the current chunk and start a new one.
|
|
326
|
+
if (current_rev_id > 0) {
|
|
327
|
+
revisions.set(current_rev_id, current_rev);
|
|
328
|
+
current_rev = '';
|
|
329
|
+
}
|
|
264
330
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
331
|
+
const rev_number = parseInt(line_identifier[1]);
|
|
332
|
+
if (isNaN(rev_number) || rev_number < 1)
|
|
333
|
+
throw new Error(rev_number + ' is not a valid revision number in ' + schema_file_lower);
|
|
334
|
+
current_rev_id = rev_number;
|
|
335
|
+
}
|
|
269
336
|
} else {
|
|
270
337
|
// Append to existing revision.
|
|
271
338
|
current_rev += line + '\n';
|
|
@@ -281,35 +348,40 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
|
|
|
281
348
|
continue;
|
|
282
349
|
}
|
|
283
350
|
|
|
351
|
+
if (deps.length > 0)
|
|
352
|
+
log('[{db}] {%s} dependencies: %s', schema_file, deps.map(e => '{' + e +'}').join(', '));
|
|
353
|
+
|
|
284
354
|
const current_schema_version = schema_versions.get(schema_name) ?? 0;
|
|
285
355
|
schema_out.push({
|
|
286
356
|
revisions,
|
|
357
|
+
file_name: schema_file_lower,
|
|
287
358
|
name: schema_name,
|
|
288
359
|
current_version: current_schema_version,
|
|
360
|
+
deps,
|
|
289
361
|
chunk_keys: Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b)
|
|
290
362
|
});
|
|
291
363
|
}
|
|
292
364
|
|
|
293
|
-
return schema_out;
|
|
365
|
+
return order_schema_dep_tree(schema_out);
|
|
294
366
|
}
|
|
295
367
|
|
|
296
|
-
export async function db_update_schema_sqlite(db: Database, schema_dir: string) {
|
|
368
|
+
export async function db_update_schema_sqlite(db: Database, schema_dir: string, schema_table_name = 'db_schema') {
|
|
297
369
|
log('[{db}] updating database schema for {%s}', db.filename);
|
|
298
370
|
|
|
299
371
|
const schema_versions = new Map();
|
|
300
372
|
|
|
301
373
|
try {
|
|
302
|
-
const query = db.query('SELECT db_schema_table_name, db_schema_version FROM
|
|
374
|
+
const query = db.query('SELECT db_schema_table_name, db_schema_version FROM ' + schema_table_name);
|
|
303
375
|
for (const row of query.all() as Array<Row_DBSchema>)
|
|
304
376
|
schema_versions.set(row.db_schema_table_name, row.db_schema_version);
|
|
305
377
|
} catch (e) {
|
|
306
|
-
log('[{db}] creating {
|
|
307
|
-
db.run(
|
|
378
|
+
log('[{db}] creating {%s} table', schema_table_name);
|
|
379
|
+
db.run(`CREATE TABLE ${schema_table_name} (db_schema_table_name TEXT PRIMARY KEY, db_schema_version INTEGER)`);
|
|
308
380
|
}
|
|
309
381
|
|
|
310
382
|
db.transaction(async () => {
|
|
311
383
|
const update_schema_query = db.prepare(`
|
|
312
|
-
INSERT INTO
|
|
384
|
+
INSERT INTO ${schema_table_name} (db_schema_version, db_schema_table_name) VALUES (?1, ?2)
|
|
313
385
|
ON CONFLICT(db_schema_table_name) DO UPDATE SET db_schema_version = EXCLUDED.db_schema_version
|
|
314
386
|
`);
|
|
315
387
|
|
|
@@ -332,7 +404,7 @@ export async function db_update_schema_sqlite(db: Database, schema_dir: string)
|
|
|
332
404
|
})();
|
|
333
405
|
}
|
|
334
406
|
|
|
335
|
-
export async function db_update_schema_mysql(db: mysql_types.Connection, schema_dir: string) {
|
|
407
|
+
export async function db_update_schema_mysql(db: mysql_types.Connection, schema_dir: string, schema_table_name = 'db_schema') {
|
|
336
408
|
if (mysql === undefined)
|
|
337
409
|
throw new Error('{db_update_schema_mysql} cannot be called without optional dependency {mysql2} installed');
|
|
338
410
|
|
|
@@ -341,18 +413,18 @@ export async function db_update_schema_mysql(db: mysql_types.Connection, schema_
|
|
|
341
413
|
const schema_versions = new Map();
|
|
342
414
|
|
|
343
415
|
try {
|
|
344
|
-
const [rows] = await db.query('SELECT db_schema_table_name, db_schema_version FROM
|
|
416
|
+
const [rows] = await db.query('SELECT db_schema_table_name, db_schema_version FROM ' + schema_table_name);
|
|
345
417
|
for (const row of rows as Array<Row_DBSchema>)
|
|
346
418
|
schema_versions.set(row.db_schema_table_name, row.db_schema_version);
|
|
347
419
|
} catch (e) {
|
|
348
|
-
log('[{db}] creating {
|
|
349
|
-
await db.query(
|
|
420
|
+
log('[{db}] creating {%s} table', schema_table_name);
|
|
421
|
+
await db.query(`CREATE TABLE ${schema_table_name} (db_schema_table_name VARCHAR(255) PRIMARY KEY, db_schema_version INT)`);
|
|
350
422
|
}
|
|
351
423
|
|
|
352
424
|
await db.beginTransaction();
|
|
353
425
|
|
|
354
426
|
const update_schema_query = await db.prepare(`
|
|
355
|
-
INSERT INTO
|
|
427
|
+
INSERT INTO ${schema_table_name} (db_schema_version, db_schema_table_name) VALUES (?, ?)
|
|
356
428
|
ON DUPLICATE KEY UPDATE db_schema_version = VALUES(db_schema_version);
|
|
357
429
|
`);
|
|
358
430
|
|
|
@@ -797,7 +869,7 @@ export function serve(port: number, hostname?: string) {
|
|
|
797
869
|
}
|
|
798
870
|
});
|
|
799
871
|
|
|
800
|
-
log('server started on port {%d}', port);
|
|
872
|
+
log('server started on port {%d} (host: {%s})', port, hostname ?? 'unspecified');
|
|
801
873
|
|
|
802
874
|
return {
|
|
803
875
|
/** Register a handler for a specific route. */
|