spooder 5.1.10 → 5.1.12

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 CHANGED
@@ -1784,6 +1784,10 @@ server.route('/', cache.file('./index.html'));
1784
1784
 
1785
1785
  // Use with server routes for dynamic content
1786
1786
  server.route('/dynamic', async (req) => cache.request(req, 'dynamic-page', () => 'Dynamic Content'));
1787
+
1788
+ // Disable caching (useful for development mode)
1789
+ const devCache = cache_http({ enabled: process.env.SPOODER_ENV !== 'dev' });
1790
+ server.route('/no-cache', devCache.file('./index.html')); // Always reads from disk
1787
1791
  ```
1788
1792
 
1789
1793
  The `cache_http()` function returns an object with two methods:
@@ -1827,7 +1831,8 @@ server.route('/api/stats', async (req) => {
1827
1831
  | `max_size` | `number` | `5242880` (5 MB) | Maximum total size of all cached files in bytes |
1828
1832
  | `use_etags` | `boolean` | `true` | Generate and use ETag headers for cache validation |
1829
1833
  | `headers` | `Record<string, string>` | `{}` | Additional HTTP headers to include in responses |
1830
- | `use_canary_reporting` | `boolean` | `false` | Reports faults to canary (see below).
1834
+ | `use_canary_reporting` | `boolean` | `false` | Reports faults to canary (see below) |
1835
+ | `enabled` | `boolean` | `true` | When false, content is generated but not stored
1831
1836
 
1832
1837
  #### Canary Reporting
1833
1838
 
@@ -2666,16 +2671,27 @@ try {
2666
2671
  ```
2667
2672
 
2668
2673
  ### Schema Dependencies
2669
- By default, schema files are executed in the order they are provided by the operating system (generally alphabetically).
2674
+ By default, schema files are executed in the order they are provided by the operating system (generally alphabetically). Individual revisions within files are always executed in ascending order.
2670
2675
 
2671
- 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.
2676
+ If a specific revision depends on one or more other schema files to be executed before it (for example, when adding foreign keys), you can specify dependencies at the revision level.
2672
2677
 
2673
2678
  ```sql
2674
- -- [deps] table_b_schema.sql, table_c_schema.sql
2675
- -- [1] create table_a
2676
- CREATE ...
2679
+ -- [1] create table_a (no dependencies)
2680
+ CREATE TABLE table_a (
2681
+ id INTEGER PRIMARY KEY,
2682
+ name TEXT NOT NULL
2683
+ );
2684
+
2685
+ -- [2] add foreign key to table_b
2686
+ -- [deps] table_b_schema.sql
2687
+ ALTER TABLE table_a ADD COLUMN table_b_id INTEGER REFERENCES table_b(id);
2677
2688
  ```
2678
2689
 
2690
+ When a revision specifies dependencies, all revisions of the dependent schema files will be executed before that specific revision runs. This allows you to create tables independently and then add dependencies in later revisions.
2691
+
2692
+ >[!IMPORTANT]
2693
+ > Dependencies are specified per-revision, not per-file. A `-- [deps]` line applies only to the revision it appears in.
2694
+
2679
2695
  >[!IMPORTANT]
2680
2696
  > Cyclic or missing dependencies will throw an error.
2681
2697
 
package/bun.lock CHANGED
@@ -14,7 +14,7 @@
14
14
  "packages": {
15
15
  "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
16
16
 
17
- "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
17
+ "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
18
18
 
19
19
  "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
20
20
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
30
30
 
31
- "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
31
+ "iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
32
32
 
33
33
  "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
34
34
 
@@ -38,7 +38,7 @@
38
38
 
39
39
  "lru.min": ["lru.min@1.1.2", "", {}, "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg=="],
40
40
 
41
- "mysql2": ["mysql2@3.14.3", "", { "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-fD6MLV8XJ1KiNFIF0bS7Msl8eZyhlTDCDl75ajU5SJtpdx9ZPEACulJcqJWr1Y8OYyxsFc4j3+nflpmhxCU5aQ=="],
41
+ "mysql2": ["mysql2@3.14.5", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-40hDf8LPUsuuJ2hFq+UgOuPwt2IFLIRDvMv6ez9hKbXeYuZPxDDwiJW7KdknvOsQqKznaKczOT1kELgFkhDvFg=="],
42
42
 
43
43
  "named-placeholders": ["named-placeholders@1.1.3", "", { "dependencies": { "lru-cache": "^7.14.1" } }, "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w=="],
44
44
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "spooder",
3
3
  "type": "module",
4
- "version": "5.1.10",
4
+ "version": "5.1.12",
5
5
  "module": "./src/api.ts",
6
6
  "bin": {
7
7
  "spooder": "./src/cli.ts"
package/src/api.ts CHANGED
@@ -128,6 +128,7 @@ type CacheOptions = {
128
128
  use_etags?: boolean;
129
129
  headers?: Record<string, string>,
130
130
  use_canary_reporting?: boolean;
131
+ enabled?: boolean;
131
132
  };
132
133
 
133
134
  type CacheEntry = {
@@ -154,6 +155,7 @@ export function cache_http(options?: CacheOptions) {
154
155
  const use_etags = options?.use_etags ?? true;
155
156
  const cache_headers = options?.headers ?? {};
156
157
  const canary_report = options?.use_canary_reporting ?? false;
158
+ const enabled = options?.enabled ?? true;
157
159
 
158
160
  const entries = new Map<string, CacheEntry>();
159
161
  let total_cache_size = 0;
@@ -267,7 +269,8 @@ export function cache_http(options?: CacheOptions) {
267
269
  cached_ts: now_ts
268
270
  };
269
271
 
270
- store_cache_entry(file_path, entry, now_ts);
272
+ if (enabled)
273
+ store_cache_entry(file_path, entry, now_ts);
271
274
  }
272
275
 
273
276
  return build_response(entry, req, 200);
@@ -290,7 +293,8 @@ export function cache_http(options?: CacheOptions) {
290
293
  cached_ts: now_ts
291
294
  };
292
295
 
293
- store_cache_entry(cache_key, entry, now_ts);
296
+ if (enabled)
297
+ store_cache_entry(cache_key, entry, now_ts);
294
298
  }
295
299
 
296
300
  return build_response(entry, req, status_code);
package/src/api_db.ts CHANGED
@@ -17,45 +17,87 @@ export function db_serialize_set<T extends string>(set: Set<T> | null): string {
17
17
  // endregion
18
18
 
19
19
  // region schema
20
- interface DependencyTarget {
20
+ interface RevisionTarget {
21
+ schema_name: string;
21
22
  file_name: string;
23
+ revision_id: number;
24
+ sql: string;
25
+ comment: string;
22
26
  deps: string[];
23
27
  }
24
28
 
25
- function order_schema_dep_tree<T extends DependencyTarget>(deps: T[]): T[] {
29
+ function order_revisions_by_dependency(schemas: any[]): RevisionTarget[] {
30
+ // First, create a map of schema names to all their pending revisions
31
+ const schema_revisions_map = new Map<string, RevisionTarget[]>();
32
+ const all_revisions: RevisionTarget[] = [];
33
+
34
+ for (const schema of schemas) {
35
+ const schema_revisions: RevisionTarget[] = [];
36
+ for (const rev_id of schema.chunk_keys) {
37
+ const revision = schema.revisions.get(rev_id);
38
+ const revision_target: RevisionTarget = {
39
+ schema_name: schema.name,
40
+ file_name: schema.file_name,
41
+ revision_id: rev_id,
42
+ sql: revision.sql,
43
+ comment: revision.comment,
44
+ deps: revision.deps || []
45
+ };
46
+ schema_revisions.push(revision_target);
47
+ all_revisions.push(revision_target);
48
+ }
49
+ schema_revisions_map.set(schema.name, schema_revisions);
50
+ }
51
+
52
+ // Now sort revisions using topological sort
26
53
  const visited = new Set<string>();
27
54
  const temp = new Set<string>();
28
- const result: T[] = [];
29
- const map = new Map(deps.map(d => [d.file_name, d]));
30
-
31
- function visit(node: T): void {
32
- if (temp.has(node.file_name))
33
- throw new Error(`Cyclic dependency {${node.file_name}}`);
34
-
35
- if (visited.has(node.file_name))
55
+ const result: RevisionTarget[] = [];
56
+
57
+ function get_revision_key(rev: RevisionTarget): string {
58
+ return `${rev.schema_name}:${rev.revision_id}`;
59
+ }
60
+
61
+ function visit(revision: RevisionTarget): void {
62
+ const rev_key = get_revision_key(revision);
63
+
64
+ if (temp.has(rev_key))
65
+ throw new Error(`Cyclic dependency detected involving ${rev_key}`);
66
+
67
+ if (visited.has(rev_key))
36
68
  return;
37
-
38
- temp.add(node.file_name);
39
-
40
- for (const dep of node.deps) {
41
- const dep_node = map.get(dep);
42
- if (!dep_node)
43
- throw new Error(`Missing dependency {${dep}}`);
69
+
70
+ temp.add(rev_key);
71
+
72
+ // Process dependencies: if this revision depends on a schema,
73
+ // all pending revisions of that schema must execute first
74
+ for (const dep_schema_file of revision.deps) {
75
+ const dep_schema_name = path.basename(dep_schema_file, '.sql');
76
+ const dep_revisions = schema_revisions_map.get(dep_schema_name);
77
+
78
+ if (!dep_revisions)
79
+ throw new Error(`Missing dependency schema {${dep_schema_name}} required by ${rev_key}`);
44
80
 
45
- visit(dep_node as T);
81
+ // Visit all pending revisions of the dependency schema
82
+ for (const dep_revision of dep_revisions) {
83
+ visit(dep_revision);
84
+ }
85
+ }
86
+
87
+ temp.delete(rev_key);
88
+ visited.add(rev_key);
89
+ result.push(revision);
90
+ }
91
+
92
+ // Visit all revisions
93
+ for (const revision of all_revisions) {
94
+ if (!visited.has(get_revision_key(revision))) {
95
+ visit(revision);
46
96
  }
47
-
48
- temp.delete(node.file_name);
49
- visited.add(node.file_name);
50
- result.push(node);
51
97
  }
52
-
53
- for (const dep of deps)
54
- if (!visited.has(dep.file_name))
55
- visit(dep);
56
-
98
+
57
99
  return result;
58
- }
100
+ }
59
101
 
60
102
  type Row_DBSchema = { db_schema_table_name: string, db_schema_version: number };
61
103
  type SchemaVersionMap = Map<string, number>;
@@ -79,26 +121,26 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
79
121
  const schema_path = path.join(schema_file_ent.parentPath, schema_file);
80
122
  const schema = await fs.readFile(schema_path, 'utf8');
81
123
 
82
- const deps = new Array<string>();
83
-
84
124
  const revisions = new Map();
85
125
  let current_rev_id = 0;
86
126
  let current_rev = '';
87
127
  let current_rev_comment = '';
128
+ let current_rev_deps = new Array<string>();
88
129
 
89
130
  for (const line of schema.split(/\r?\n/)) {
90
131
  const line_identifier = line.match(/^--\s*\[(\d+|deps)\]/);
91
132
  if (line_identifier !== null) {
92
133
  if (line_identifier[1] === 'deps') {
93
- // Line contains schema dependencies, example: -- [deps] schema_b.sql,schema_c.sql
134
+ // Line contains schema dependencies for current revision, example: -- [deps] schema_b.sql,schema_c.sql
94
135
  const deps_raw = line.substring(line.indexOf(']') + 1);
95
- deps.push(...deps_raw.split(',').map(e => e.trim().toLowerCase()));
136
+ current_rev_deps.push(...deps_raw.split(',').map(e => e.trim().toLowerCase()));
96
137
  } else {
97
138
  // New chunk definition detected, store the current chunk and start a new one.
98
139
  if (current_rev_id > 0) {
99
- revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment });
140
+ revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment, deps: current_rev_deps });
100
141
  current_rev = '';
101
142
  current_rev_comment = '';
143
+ current_rev_deps = new Array<string>();
102
144
  }
103
145
 
104
146
  const rev_number = parseInt(line_identifier[1]);
@@ -118,28 +160,33 @@ async function db_load_schema(schema_dir: string, schema_versions: SchemaVersion
118
160
 
119
161
  // There may be something left in current_chunk once we reach end of the file.
120
162
  if (current_rev_id > 0)
121
- revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment });
163
+ revisions.set(current_rev_id, { sql: current_rev, comment: current_rev_comment, deps: current_rev_deps });
122
164
 
123
165
  if (revisions.size === 0) {
124
166
  db_log(`{${schema_file}} contains no valid revisions`);
125
167
  continue;
126
168
  }
127
169
 
128
- if (deps.length > 0)
129
- db_log(`{${schema_file}} dependencies: ${log_list(deps)}`);
130
-
131
170
  const current_schema_version = schema_versions.get(schema_name) ?? 0;
171
+ const pending_revisions = Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b);
172
+
173
+ // Log any revision-level dependencies
174
+ for (const rev_id of pending_revisions) {
175
+ const revision = revisions.get(rev_id);
176
+ if (revision.deps.length > 0)
177
+ db_log(`{${schema_file}} revision [${rev_id}] dependencies: ${log_list(revision.deps)}`);
178
+ }
179
+
132
180
  schema_out.push({
133
181
  revisions,
134
182
  file_name: schema_file_lower,
135
183
  name: schema_name,
136
184
  current_version: current_schema_version,
137
- deps,
138
- chunk_keys: Array.from(revisions.keys()).filter(chunk_id => chunk_id > current_schema_version).sort((a, b) => a - b)
185
+ chunk_keys: pending_revisions
139
186
  });
140
187
  }
141
188
 
142
- return order_schema_dep_tree(schema_out);
189
+ return schema_out;
143
190
  }
144
191
  // endregion
145
192
 
@@ -179,20 +226,28 @@ export async function db_update_schema_mysql(db: mysql_types.Connection, schema_
179
226
  `);
180
227
 
181
228
  const schemas = await db_load_schema(schema_dir, schema_versions);
182
- for (const schema of schemas) {
183
- let newest_schema_version = schema.current_version;
184
- for (const rev_id of schema.chunk_keys) {
185
- const revision = schema.revisions.get(rev_id);
186
- const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
187
- db_log(`applying revision [{${rev_id}}]${comment_text} to {${schema.name}}`);
188
-
189
- await db.query(revision.sql);
190
- newest_schema_version = rev_id;
191
- }
229
+ const revisions = order_revisions_by_dependency(schemas);
230
+
231
+ // Track the newest version for each schema
232
+ const schema_versions_tracking = new Map<string, number>();
233
+ for (const schema of schemas)
234
+ schema_versions_tracking.set(schema.name, schema.current_version);
235
+
236
+ // Execute revisions in dependency order
237
+ for (const revision of revisions) {
238
+ const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
239
+ db_log(`applying revision [{${revision.revision_id}}]${comment_text} to {${revision.schema_name}}`);
240
+
241
+ await db.query(revision.sql);
242
+ schema_versions_tracking.set(revision.schema_name, revision.revision_id);
243
+ }
192
244
 
193
- if (newest_schema_version > schema.current_version) {
194
- db_log(`updated table {${schema.name}} to revision {${newest_schema_version}}`);
195
- await update_schema_query.execute([newest_schema_version, schema.name]);
245
+ // Update schema version tracking for all modified schemas
246
+ for (const [schema_name, newest_version] of schema_versions_tracking) {
247
+ const original_version = schemas.find(s => s.name === schema_name)?.current_version || 0;
248
+ if (newest_version > original_version) {
249
+ db_log(`updated table {${schema_name}} to revision {${newest_version}}`);
250
+ await update_schema_query.execute([newest_version, schema_name]);
196
251
  }
197
252
  }
198
253
 
@@ -451,20 +506,27 @@ export async function db_update_schema_sqlite(db: Database, schema_dir: string,
451
506
  `);
452
507
 
453
508
  const schemas = await db_load_schema(schema_dir, schema_versions);
454
-
455
- for (const schema of schemas) {
456
- let newest_schema_version = schema.current_version;
457
- for (const rev_id of schema.chunk_keys) {
458
- const revision = schema.revisions.get(rev_id);
459
- const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
460
- db_log(`applying revision [{${rev_id}}]${comment_text} to {${schema.name}}`);
461
- db.transaction(() => db.run(revision.sql))();
462
- newest_schema_version = rev_id;
463
- }
464
-
465
- if (newest_schema_version > schema.current_version) {
466
- db_log(`updated table {${schema.name}} to revision {${newest_schema_version}}`);
467
- update_schema_query.run(newest_schema_version, schema.name);
509
+ const revisions = order_revisions_by_dependency(schemas);
510
+
511
+ // Track the newest version for each schema
512
+ const schema_versions_tracking = new Map<string, number>();
513
+ for (const schema of schemas)
514
+ schema_versions_tracking.set(schema.name, schema.current_version);
515
+
516
+ // Execute revisions in dependency order
517
+ for (const revision of revisions) {
518
+ const comment_text = revision.comment ? ` "{${revision.comment}}"` : '';
519
+ db_log(`applying revision [{${revision.revision_id}}]${comment_text} to {${revision.schema_name}}`);
520
+ db.transaction(() => db.run(revision.sql))();
521
+ schema_versions_tracking.set(revision.schema_name, revision.revision_id);
522
+ }
523
+
524
+ // Update schema version tracking for all modified schemas
525
+ for (const [schema_name, newest_version] of schema_versions_tracking) {
526
+ const original_version = schemas.find(s => s.name === schema_name)?.current_version || 0;
527
+ if (newest_version > original_version) {
528
+ db_log(`updated table {${schema_name}} to revision {${newest_version}}`);
529
+ update_schema_query.run(newest_version, schema_name);
468
530
  }
469
531
  }
470
532