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.
Files changed (4) hide show
  1. package/README.md +52 -1
  2. package/bun.lock +51 -0
  3. package/package.json +1 -1
  4. 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. Schema revisions are tracked in a table called `db_schema` which is created automatically if it does not exist with the following schema.
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "4.5.7",
4
+ "version": "4.5.9",
5
5
  "exports": {
6
6
  ".": {
7
7
  "bun": "./src/api.ts",
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 rev_start = line.match(/^--\s*\[(\d+)\]/);
258
- if (rev_start !== null) {
259
- // New chunk definition detected, store the current chunk and start a new one.
260
- if (current_rev_id > 0) {
261
- revisions.set(current_rev_id, current_rev);
262
- current_rev = '';
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
- const rev_number = parseInt(rev_start[1]);
266
- if (isNaN(rev_number) || rev_number < 1)
267
- throw new Error(rev_number + ' is not a valid revision number in ' + schema_file_lower);
268
- current_rev_id = rev_number;
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 db_schema');
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 {db_schema} table');
307
- db.run('CREATE TABLE db_schema (db_schema_table_name TEXT PRIMARY KEY, db_schema_version INTEGER)');
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 db_schema (db_schema_version, db_schema_table_name) VALUES (?1, ?2)
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 db_schema');
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 {db_schema} table');
349
- await db.query('CREATE TABLE db_schema (db_schema_table_name VARCHAR(255) PRIMARY KEY, db_schema_version INT)');
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 db_schema (db_schema_version, db_schema_table_name) VALUES (?, ?)
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. */