latticesql 0.16.2 → 0.17.1
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 +79 -5
- package/dist/cli.js +60 -6
- package/dist/index.cjs +60 -6
- package/dist/index.d.cts +24 -3
- package/dist/index.d.ts +24 -3
- package/dist/index.js +60 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ Lattice has no opinions about your schema, your agents, or your file format. You
|
|
|
35
35
|
- [defineEntityContext()](#defineentitycontext-v05)
|
|
36
36
|
- [defineWriteback()](#definewriteback)
|
|
37
37
|
- [init() / close()](#init--close)
|
|
38
|
+
- [migrate()](#migrate-v017)
|
|
38
39
|
- [CRUD operations](#crud-operations)
|
|
39
40
|
- [Query operators](#query-operators)
|
|
40
41
|
- [Render, sync, watch, and reconcile](#render-sync-watch-and-reconcile)
|
|
@@ -202,11 +203,12 @@ interface TableDefinition {
|
|
|
202
203
|
* - A render function: (rows: Row[]) => string
|
|
203
204
|
* - A built-in template name: 'default-list' | 'default-table' | 'default-detail' | 'default-json'
|
|
204
205
|
* - A template spec with hooks: { template: BuiltinTemplateName, hooks?: RenderHooks }
|
|
206
|
+
* Optional (v0.17+) — omit render and outputFile for schema-only tables.
|
|
205
207
|
*/
|
|
206
|
-
render
|
|
208
|
+
render?: RenderSpec;
|
|
207
209
|
|
|
208
|
-
/** Output file path, relative to the outputDir passed to render()/watch() */
|
|
209
|
-
outputFile
|
|
210
|
+
/** Output file path, relative to the outputDir passed to render()/watch(). Optional (v0.17+). */
|
|
211
|
+
outputFile?: string;
|
|
210
212
|
|
|
211
213
|
/** Optional row filter applied before rendering */
|
|
212
214
|
filter?: (rows: Row[]) => Row[];
|
|
@@ -215,10 +217,12 @@ interface TableDefinition {
|
|
|
215
217
|
* Primary key column name or [col1, col2] for composite PKs.
|
|
216
218
|
* Defaults to 'id'. When 'id' is the PK and the field is absent on insert,
|
|
217
219
|
* a UUID v4 is generated automatically.
|
|
220
|
+
* Composite PKs (v0.17+): auto-generates a PRIMARY KEY(...) constraint —
|
|
221
|
+
* no need to add it manually via tableConstraints.
|
|
218
222
|
*/
|
|
219
223
|
primaryKey?: string | string[];
|
|
220
224
|
|
|
221
|
-
/** Additional SQL constraints (required for composite PKs) */
|
|
225
|
+
/** Additional SQL constraints (e.g., UNIQUE, CHECK). No longer required for composite PKs (v0.17+). */
|
|
222
226
|
tableConstraints?: string[];
|
|
223
227
|
|
|
224
228
|
/** Declared relationships used by template rendering */
|
|
@@ -267,6 +271,22 @@ await db.update('pages', 'about-us', { title: 'About' });
|
|
|
267
271
|
await db.delete('pages', 'about-us');
|
|
268
272
|
```
|
|
269
273
|
|
|
274
|
+
**Schema-only table (v0.17+):**
|
|
275
|
+
|
|
276
|
+
Tables without `render` and `outputFile` get full schema support (columns, indexes, constraints, CRUD) but produce no output files during `render()` or `watch()`. Useful for junction tables, internal tracking tables, or any table that doesn't need a context file.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
db.define('agent_skills', {
|
|
280
|
+
columns: {
|
|
281
|
+
agent_id: 'TEXT NOT NULL',
|
|
282
|
+
skill_id: 'TEXT NOT NULL',
|
|
283
|
+
proficiency: 'TEXT DEFAULT "basic"',
|
|
284
|
+
},
|
|
285
|
+
primaryKey: ['agent_id', 'skill_id'],
|
|
286
|
+
// No render, no outputFile — schema-only
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
270
290
|
**Composite primary key:**
|
|
271
291
|
|
|
272
292
|
```typescript
|
|
@@ -276,7 +296,6 @@ db.define('event_seats', {
|
|
|
276
296
|
seat_no: 'INTEGER NOT NULL',
|
|
277
297
|
holder: 'TEXT',
|
|
278
298
|
},
|
|
279
|
-
tableConstraints: ['PRIMARY KEY (event_id, seat_no)'],
|
|
280
299
|
primaryKey: ['event_id', 'seat_no'],
|
|
281
300
|
render: 'default-table',
|
|
282
301
|
outputFile: 'seats.md',
|
|
@@ -835,6 +854,26 @@ Migrations are idempotent — each `version` number is applied exactly once, tra
|
|
|
835
854
|
|
|
836
855
|
`close()` closes the SQLite connection. Call it when the process shuts down.
|
|
837
856
|
|
|
857
|
+
### `migrate()` (v0.17+)
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
await db.migrate(migrations: Migration[]): Promise<void>
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Run migrations after `init()`. Works exactly like `init({ migrations })` but callable any time — useful when migrations are loaded dynamically or added by plugins after startup.
|
|
864
|
+
|
|
865
|
+
```typescript
|
|
866
|
+
await db.init();
|
|
867
|
+
|
|
868
|
+
// Later — e.g., after loading a plugin that needs new columns
|
|
869
|
+
await db.migrate([
|
|
870
|
+
{ version: 'plugin-v1', sql: 'ALTER TABLE tasks ADD COLUMN tags TEXT' },
|
|
871
|
+
{ version: 'plugin-v2', sql: 'CREATE INDEX IF NOT EXISTS idx_tasks_tags ON tasks (tags)' },
|
|
872
|
+
]);
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
`Migration.version` accepts `number | string` — use numbers for sequential migrations, or strings for named/namespaced versions (e.g., `'plugin-v1'`). Each version is applied at most once, tracked in the same `__lattice_migrations` table used by `init()`.
|
|
876
|
+
|
|
838
877
|
---
|
|
839
878
|
|
|
840
879
|
### CRUD operations
|
|
@@ -860,6 +899,24 @@ await db.insert('pages', { slug: 'about', title: 'About Us' });
|
|
|
860
899
|
await db.insert('tasks', { id: 'task-001', title: 'Specific task' });
|
|
861
900
|
```
|
|
862
901
|
|
|
902
|
+
#### `insertReturning()` (v0.17+)
|
|
903
|
+
|
|
904
|
+
```typescript
|
|
905
|
+
await db.insertReturning(table: string, row: Row): Promise<Row>
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
Insert a row and get the full row back — including the auto-generated `id`, defaults, and any other columns. Equivalent to `insert()` + `get()` in a single call.
|
|
909
|
+
|
|
910
|
+
```typescript
|
|
911
|
+
const task = await db.insertReturning('tasks', { title: 'Write docs', status: 'open' });
|
|
912
|
+
// task → { id: 'f47ac10b-...', title: 'Write docs', status: 'open', priority: 0, ... }
|
|
913
|
+
|
|
914
|
+
// Useful when you need the generated id or default values immediately
|
|
915
|
+
const agent = await db.insertReturning('agents', { name: 'Gamma' });
|
|
916
|
+
console.log(agent.id); // auto-generated UUID
|
|
917
|
+
console.log(agent.active); // default value from schema
|
|
918
|
+
```
|
|
919
|
+
|
|
863
920
|
#### `upsert()`
|
|
864
921
|
|
|
865
922
|
```typescript
|
|
@@ -899,6 +956,23 @@ await db.update('tasks', 'task-001', { status: 'done' });
|
|
|
899
956
|
await db.update('event_seats', { event_id: 'e-1', seat_no: 3 }, { holder: 'Bob' });
|
|
900
957
|
```
|
|
901
958
|
|
|
959
|
+
#### `updateReturning()` (v0.17+)
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
await db.updateReturning(table: string, id: PkLookup, row: Partial<Row>): Promise<Row>
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
Update specific columns and get the full updated row back. Equivalent to `update()` + `get()` in a single call.
|
|
966
|
+
|
|
967
|
+
```typescript
|
|
968
|
+
const task = await db.updateReturning('tasks', 'task-001', { status: 'done' });
|
|
969
|
+
// task → { id: 'task-001', title: 'Write docs', status: 'done', priority: 3, ... }
|
|
970
|
+
|
|
971
|
+
// Composite PK
|
|
972
|
+
const seat = await db.updateReturning('event_seats', { event_id: 'e-1', seat_no: 3 }, { holder: 'Bob' });
|
|
973
|
+
// seat → { event_id: 'e-1', seat_no: 3, holder: 'Bob' }
|
|
974
|
+
```
|
|
975
|
+
|
|
902
976
|
#### `delete()`
|
|
903
977
|
|
|
904
978
|
```typescript
|
package/dist/cli.js
CHANGED
|
@@ -492,24 +492,37 @@ var SchemaManager = class {
|
|
|
492
492
|
*/
|
|
493
493
|
applySchema(adapter) {
|
|
494
494
|
for (const [name, def] of this._tables) {
|
|
495
|
-
this.
|
|
495
|
+
const pkCols = this._tablePK.get(name) ?? ["id"];
|
|
496
|
+
let constraints = def.tableConstraints ? [...def.tableConstraints] : [];
|
|
497
|
+
if (pkCols.length > 1) {
|
|
498
|
+
const alreadyHasPK = constraints.some((c) => c.toUpperCase().startsWith("PRIMARY KEY"));
|
|
499
|
+
if (!alreadyHasPK) {
|
|
500
|
+
constraints.unshift(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
this._ensureTable(adapter, name, def.columns, constraints.length ? constraints : void 0);
|
|
496
504
|
}
|
|
497
505
|
this._ensureTable(adapter, "__lattice_migrations", {
|
|
498
|
-
version: "
|
|
506
|
+
version: "TEXT PRIMARY KEY",
|
|
499
507
|
applied_at: "TEXT NOT NULL"
|
|
500
508
|
});
|
|
501
509
|
}
|
|
502
510
|
/** Run explicit versioned migrations in order, idempotently */
|
|
503
511
|
applyMigrations(adapter, migrations) {
|
|
504
|
-
const sorted = [...migrations].sort((a, b) =>
|
|
512
|
+
const sorted = [...migrations].sort((a, b) => {
|
|
513
|
+
const va = String(a.version);
|
|
514
|
+
const vb = String(b.version);
|
|
515
|
+
return va.localeCompare(vb, void 0, { numeric: true });
|
|
516
|
+
});
|
|
505
517
|
for (const m of sorted) {
|
|
518
|
+
const versionStr = String(m.version);
|
|
506
519
|
const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
|
|
507
|
-
|
|
520
|
+
versionStr
|
|
508
521
|
]);
|
|
509
522
|
if (!exists) {
|
|
510
523
|
adapter.run(m.sql);
|
|
511
524
|
adapter.run("INSERT INTO __lattice_migrations (version, applied_at) VALUES (?, ?)", [
|
|
512
|
-
|
|
525
|
+
versionStr,
|
|
513
526
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
514
527
|
]);
|
|
515
528
|
}
|
|
@@ -544,6 +557,7 @@ var SchemaManager = class {
|
|
|
544
557
|
const existing = adapter.all(`PRAGMA table_info("${table}")`).map((r) => r.name);
|
|
545
558
|
for (const [col, type] of Object.entries(columns)) {
|
|
546
559
|
if (!existing.includes(col)) {
|
|
560
|
+
if (type.toUpperCase().includes("PRIMARY KEY")) continue;
|
|
547
561
|
adapter.run(`ALTER TABLE "${table}" ADD COLUMN "${col}" ${type}`);
|
|
548
562
|
}
|
|
549
563
|
}
|
|
@@ -1612,7 +1626,8 @@ var Lattice = class {
|
|
|
1612
1626
|
this._assertNotInit("define");
|
|
1613
1627
|
const compiledDef = {
|
|
1614
1628
|
...def,
|
|
1615
|
-
render: compileRender(def, table, this._schema, this._adapter)
|
|
1629
|
+
render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
|
|
1630
|
+
outputFile: def.outputFile ?? `.schema-only/${table}.md`
|
|
1616
1631
|
};
|
|
1617
1632
|
this._schema.define(table, compiledDef);
|
|
1618
1633
|
return this;
|
|
@@ -1654,6 +1669,23 @@ var Lattice = class {
|
|
|
1654
1669
|
this._initialized = true;
|
|
1655
1670
|
return Promise.resolve();
|
|
1656
1671
|
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Run additional migrations after init(). Useful for package-level schema
|
|
1674
|
+
* changes applied at runtime (e.g. update hooks that add columns).
|
|
1675
|
+
*
|
|
1676
|
+
* @since 0.17.0
|
|
1677
|
+
*/
|
|
1678
|
+
migrate(migrations) {
|
|
1679
|
+
if (!this._initialized) {
|
|
1680
|
+
return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
|
|
1681
|
+
}
|
|
1682
|
+
this._schema.applyMigrations(this._adapter, migrations);
|
|
1683
|
+
for (const tableName of this._schema.getTables().keys()) {
|
|
1684
|
+
const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
|
|
1685
|
+
this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
|
|
1686
|
+
}
|
|
1687
|
+
return Promise.resolve();
|
|
1688
|
+
}
|
|
1657
1689
|
close() {
|
|
1658
1690
|
this._adapter.close();
|
|
1659
1691
|
this._columnCache.clear();
|
|
@@ -1686,6 +1718,17 @@ var Lattice = class {
|
|
|
1686
1718
|
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
1687
1719
|
return Promise.resolve(pkValue);
|
|
1688
1720
|
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Insert a row and return the full inserted row (including auto-generated
|
|
1723
|
+
* fields and defaults). Equivalent to `insert()` followed by `get()`.
|
|
1724
|
+
*
|
|
1725
|
+
* @since 0.17.0
|
|
1726
|
+
*/
|
|
1727
|
+
insertReturning(table, row) {
|
|
1728
|
+
return this.insert(table, row).then(
|
|
1729
|
+
(pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1689
1732
|
upsert(table, row) {
|
|
1690
1733
|
const notInit = this._notInitError();
|
|
1691
1734
|
if (notInit) return notInit;
|
|
@@ -1740,6 +1783,17 @@ var Lattice = class {
|
|
|
1740
1783
|
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
1741
1784
|
return Promise.resolve();
|
|
1742
1785
|
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Update a row and return the full updated row. Equivalent to `update()`
|
|
1788
|
+
* followed by `get()`.
|
|
1789
|
+
*
|
|
1790
|
+
* @since 0.17.0
|
|
1791
|
+
*/
|
|
1792
|
+
updateReturning(table, id, row) {
|
|
1793
|
+
return this.update(table, id, row).then(
|
|
1794
|
+
() => this.get(table, id).then((result) => result ?? row)
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1743
1797
|
delete(table, id) {
|
|
1744
1798
|
const notInit = this._notInitError();
|
|
1745
1799
|
if (notInit) return notInit;
|
package/dist/index.cjs
CHANGED
|
@@ -240,24 +240,37 @@ var SchemaManager = class {
|
|
|
240
240
|
*/
|
|
241
241
|
applySchema(adapter) {
|
|
242
242
|
for (const [name, def] of this._tables) {
|
|
243
|
-
this.
|
|
243
|
+
const pkCols = this._tablePK.get(name) ?? ["id"];
|
|
244
|
+
let constraints = def.tableConstraints ? [...def.tableConstraints] : [];
|
|
245
|
+
if (pkCols.length > 1) {
|
|
246
|
+
const alreadyHasPK = constraints.some((c) => c.toUpperCase().startsWith("PRIMARY KEY"));
|
|
247
|
+
if (!alreadyHasPK) {
|
|
248
|
+
constraints.unshift(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
this._ensureTable(adapter, name, def.columns, constraints.length ? constraints : void 0);
|
|
244
252
|
}
|
|
245
253
|
this._ensureTable(adapter, "__lattice_migrations", {
|
|
246
|
-
version: "
|
|
254
|
+
version: "TEXT PRIMARY KEY",
|
|
247
255
|
applied_at: "TEXT NOT NULL"
|
|
248
256
|
});
|
|
249
257
|
}
|
|
250
258
|
/** Run explicit versioned migrations in order, idempotently */
|
|
251
259
|
applyMigrations(adapter, migrations) {
|
|
252
|
-
const sorted = [...migrations].sort((a, b) =>
|
|
260
|
+
const sorted = [...migrations].sort((a, b) => {
|
|
261
|
+
const va = String(a.version);
|
|
262
|
+
const vb = String(b.version);
|
|
263
|
+
return va.localeCompare(vb, void 0, { numeric: true });
|
|
264
|
+
});
|
|
253
265
|
for (const m of sorted) {
|
|
266
|
+
const versionStr = String(m.version);
|
|
254
267
|
const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
|
|
255
|
-
|
|
268
|
+
versionStr
|
|
256
269
|
]);
|
|
257
270
|
if (!exists) {
|
|
258
271
|
adapter.run(m.sql);
|
|
259
272
|
adapter.run("INSERT INTO __lattice_migrations (version, applied_at) VALUES (?, ?)", [
|
|
260
|
-
|
|
273
|
+
versionStr,
|
|
261
274
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
262
275
|
]);
|
|
263
276
|
}
|
|
@@ -292,6 +305,7 @@ var SchemaManager = class {
|
|
|
292
305
|
const existing = adapter.all(`PRAGMA table_info("${table}")`).map((r) => r.name);
|
|
293
306
|
for (const [col, type] of Object.entries(columns)) {
|
|
294
307
|
if (!existing.includes(col)) {
|
|
308
|
+
if (type.toUpperCase().includes("PRIMARY KEY")) continue;
|
|
295
309
|
adapter.run(`ALTER TABLE "${table}" ADD COLUMN "${col}" ${type}`);
|
|
296
310
|
}
|
|
297
311
|
}
|
|
@@ -1648,7 +1662,8 @@ var Lattice = class {
|
|
|
1648
1662
|
this._assertNotInit("define");
|
|
1649
1663
|
const compiledDef = {
|
|
1650
1664
|
...def,
|
|
1651
|
-
render: compileRender(def, table, this._schema, this._adapter)
|
|
1665
|
+
render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
|
|
1666
|
+
outputFile: def.outputFile ?? `.schema-only/${table}.md`
|
|
1652
1667
|
};
|
|
1653
1668
|
this._schema.define(table, compiledDef);
|
|
1654
1669
|
return this;
|
|
@@ -1690,6 +1705,23 @@ var Lattice = class {
|
|
|
1690
1705
|
this._initialized = true;
|
|
1691
1706
|
return Promise.resolve();
|
|
1692
1707
|
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Run additional migrations after init(). Useful for package-level schema
|
|
1710
|
+
* changes applied at runtime (e.g. update hooks that add columns).
|
|
1711
|
+
*
|
|
1712
|
+
* @since 0.17.0
|
|
1713
|
+
*/
|
|
1714
|
+
migrate(migrations) {
|
|
1715
|
+
if (!this._initialized) {
|
|
1716
|
+
return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
|
|
1717
|
+
}
|
|
1718
|
+
this._schema.applyMigrations(this._adapter, migrations);
|
|
1719
|
+
for (const tableName of this._schema.getTables().keys()) {
|
|
1720
|
+
const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
|
|
1721
|
+
this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
|
|
1722
|
+
}
|
|
1723
|
+
return Promise.resolve();
|
|
1724
|
+
}
|
|
1693
1725
|
close() {
|
|
1694
1726
|
this._adapter.close();
|
|
1695
1727
|
this._columnCache.clear();
|
|
@@ -1722,6 +1754,17 @@ var Lattice = class {
|
|
|
1722
1754
|
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
1723
1755
|
return Promise.resolve(pkValue);
|
|
1724
1756
|
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Insert a row and return the full inserted row (including auto-generated
|
|
1759
|
+
* fields and defaults). Equivalent to `insert()` followed by `get()`.
|
|
1760
|
+
*
|
|
1761
|
+
* @since 0.17.0
|
|
1762
|
+
*/
|
|
1763
|
+
insertReturning(table, row) {
|
|
1764
|
+
return this.insert(table, row).then(
|
|
1765
|
+
(pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1725
1768
|
upsert(table, row) {
|
|
1726
1769
|
const notInit = this._notInitError();
|
|
1727
1770
|
if (notInit) return notInit;
|
|
@@ -1776,6 +1819,17 @@ var Lattice = class {
|
|
|
1776
1819
|
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
1777
1820
|
return Promise.resolve();
|
|
1778
1821
|
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Update a row and return the full updated row. Equivalent to `update()`
|
|
1824
|
+
* followed by `get()`.
|
|
1825
|
+
*
|
|
1826
|
+
* @since 0.17.0
|
|
1827
|
+
*/
|
|
1828
|
+
updateReturning(table, id, row) {
|
|
1829
|
+
return this.update(table, id, row).then(
|
|
1830
|
+
() => this.get(table, id).then((result) => result ?? row)
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1779
1833
|
delete(table, id) {
|
|
1780
1834
|
const notInit = this._notInitError();
|
|
1781
1835
|
if (notInit) return notInit;
|
package/dist/index.d.cts
CHANGED
|
@@ -714,9 +714,9 @@ interface TableDefinition {
|
|
|
714
714
|
* `'default-detail'`, `'default-json'`) to use a built-in template.
|
|
715
715
|
* - Pass a `TemplateRenderSpec` to use a built-in template with lifecycle hooks.
|
|
716
716
|
*/
|
|
717
|
-
render
|
|
717
|
+
render?: RenderSpec;
|
|
718
718
|
/** Output path relative to the outputDir passed to render/watch */
|
|
719
|
-
outputFile
|
|
719
|
+
outputFile?: string;
|
|
720
720
|
/** Optional pre-filter applied before render */
|
|
721
721
|
filter?: (rows: Row[]) => Row[];
|
|
722
722
|
/**
|
|
@@ -825,7 +825,7 @@ interface InitOptions {
|
|
|
825
825
|
migrations?: Migration[];
|
|
826
826
|
}
|
|
827
827
|
interface Migration {
|
|
828
|
-
version: number;
|
|
828
|
+
version: number | string;
|
|
829
829
|
sql: string;
|
|
830
830
|
}
|
|
831
831
|
interface WatchOptions {
|
|
@@ -1110,11 +1110,32 @@ declare class Lattice {
|
|
|
1110
1110
|
defineWriteHook(hook: WriteHook): this;
|
|
1111
1111
|
defineWriteback(def: WritebackDefinition): this;
|
|
1112
1112
|
init(options?: InitOptions): Promise<void>;
|
|
1113
|
+
/**
|
|
1114
|
+
* Run additional migrations after init(). Useful for package-level schema
|
|
1115
|
+
* changes applied at runtime (e.g. update hooks that add columns).
|
|
1116
|
+
*
|
|
1117
|
+
* @since 0.17.0
|
|
1118
|
+
*/
|
|
1119
|
+
migrate(migrations: Migration[]): Promise<void>;
|
|
1113
1120
|
close(): void;
|
|
1114
1121
|
insert(table: string, row: Row): Promise<string>;
|
|
1122
|
+
/**
|
|
1123
|
+
* Insert a row and return the full inserted row (including auto-generated
|
|
1124
|
+
* fields and defaults). Equivalent to `insert()` followed by `get()`.
|
|
1125
|
+
*
|
|
1126
|
+
* @since 0.17.0
|
|
1127
|
+
*/
|
|
1128
|
+
insertReturning(table: string, row: Row): Promise<Row>;
|
|
1115
1129
|
upsert(table: string, row: Row): Promise<string>;
|
|
1116
1130
|
upsertBy(table: string, col: string, val: unknown, row: Row): Promise<string>;
|
|
1117
1131
|
update(table: string, id: PkLookup, row: Partial<Row>): Promise<void>;
|
|
1132
|
+
/**
|
|
1133
|
+
* Update a row and return the full updated row. Equivalent to `update()`
|
|
1134
|
+
* followed by `get()`.
|
|
1135
|
+
*
|
|
1136
|
+
* @since 0.17.0
|
|
1137
|
+
*/
|
|
1138
|
+
updateReturning(table: string, id: PkLookup, row: Partial<Row>): Promise<Row>;
|
|
1118
1139
|
delete(table: string, id: PkLookup): Promise<void>;
|
|
1119
1140
|
get(table: string, id: PkLookup): Promise<Row | null>;
|
|
1120
1141
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -714,9 +714,9 @@ interface TableDefinition {
|
|
|
714
714
|
* `'default-detail'`, `'default-json'`) to use a built-in template.
|
|
715
715
|
* - Pass a `TemplateRenderSpec` to use a built-in template with lifecycle hooks.
|
|
716
716
|
*/
|
|
717
|
-
render
|
|
717
|
+
render?: RenderSpec;
|
|
718
718
|
/** Output path relative to the outputDir passed to render/watch */
|
|
719
|
-
outputFile
|
|
719
|
+
outputFile?: string;
|
|
720
720
|
/** Optional pre-filter applied before render */
|
|
721
721
|
filter?: (rows: Row[]) => Row[];
|
|
722
722
|
/**
|
|
@@ -825,7 +825,7 @@ interface InitOptions {
|
|
|
825
825
|
migrations?: Migration[];
|
|
826
826
|
}
|
|
827
827
|
interface Migration {
|
|
828
|
-
version: number;
|
|
828
|
+
version: number | string;
|
|
829
829
|
sql: string;
|
|
830
830
|
}
|
|
831
831
|
interface WatchOptions {
|
|
@@ -1110,11 +1110,32 @@ declare class Lattice {
|
|
|
1110
1110
|
defineWriteHook(hook: WriteHook): this;
|
|
1111
1111
|
defineWriteback(def: WritebackDefinition): this;
|
|
1112
1112
|
init(options?: InitOptions): Promise<void>;
|
|
1113
|
+
/**
|
|
1114
|
+
* Run additional migrations after init(). Useful for package-level schema
|
|
1115
|
+
* changes applied at runtime (e.g. update hooks that add columns).
|
|
1116
|
+
*
|
|
1117
|
+
* @since 0.17.0
|
|
1118
|
+
*/
|
|
1119
|
+
migrate(migrations: Migration[]): Promise<void>;
|
|
1113
1120
|
close(): void;
|
|
1114
1121
|
insert(table: string, row: Row): Promise<string>;
|
|
1122
|
+
/**
|
|
1123
|
+
* Insert a row and return the full inserted row (including auto-generated
|
|
1124
|
+
* fields and defaults). Equivalent to `insert()` followed by `get()`.
|
|
1125
|
+
*
|
|
1126
|
+
* @since 0.17.0
|
|
1127
|
+
*/
|
|
1128
|
+
insertReturning(table: string, row: Row): Promise<Row>;
|
|
1115
1129
|
upsert(table: string, row: Row): Promise<string>;
|
|
1116
1130
|
upsertBy(table: string, col: string, val: unknown, row: Row): Promise<string>;
|
|
1117
1131
|
update(table: string, id: PkLookup, row: Partial<Row>): Promise<void>;
|
|
1132
|
+
/**
|
|
1133
|
+
* Update a row and return the full updated row. Equivalent to `update()`
|
|
1134
|
+
* followed by `get()`.
|
|
1135
|
+
*
|
|
1136
|
+
* @since 0.17.0
|
|
1137
|
+
*/
|
|
1138
|
+
updateReturning(table: string, id: PkLookup, row: Partial<Row>): Promise<Row>;
|
|
1118
1139
|
delete(table: string, id: PkLookup): Promise<void>;
|
|
1119
1140
|
get(table: string, id: PkLookup): Promise<Row | null>;
|
|
1120
1141
|
/**
|
package/dist/index.js
CHANGED
|
@@ -178,24 +178,37 @@ var SchemaManager = class {
|
|
|
178
178
|
*/
|
|
179
179
|
applySchema(adapter) {
|
|
180
180
|
for (const [name, def] of this._tables) {
|
|
181
|
-
this.
|
|
181
|
+
const pkCols = this._tablePK.get(name) ?? ["id"];
|
|
182
|
+
let constraints = def.tableConstraints ? [...def.tableConstraints] : [];
|
|
183
|
+
if (pkCols.length > 1) {
|
|
184
|
+
const alreadyHasPK = constraints.some((c) => c.toUpperCase().startsWith("PRIMARY KEY"));
|
|
185
|
+
if (!alreadyHasPK) {
|
|
186
|
+
constraints.unshift(`PRIMARY KEY (${pkCols.map((c) => `"${c}"`).join(", ")})`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
this._ensureTable(adapter, name, def.columns, constraints.length ? constraints : void 0);
|
|
182
190
|
}
|
|
183
191
|
this._ensureTable(adapter, "__lattice_migrations", {
|
|
184
|
-
version: "
|
|
192
|
+
version: "TEXT PRIMARY KEY",
|
|
185
193
|
applied_at: "TEXT NOT NULL"
|
|
186
194
|
});
|
|
187
195
|
}
|
|
188
196
|
/** Run explicit versioned migrations in order, idempotently */
|
|
189
197
|
applyMigrations(adapter, migrations) {
|
|
190
|
-
const sorted = [...migrations].sort((a, b) =>
|
|
198
|
+
const sorted = [...migrations].sort((a, b) => {
|
|
199
|
+
const va = String(a.version);
|
|
200
|
+
const vb = String(b.version);
|
|
201
|
+
return va.localeCompare(vb, void 0, { numeric: true });
|
|
202
|
+
});
|
|
191
203
|
for (const m of sorted) {
|
|
204
|
+
const versionStr = String(m.version);
|
|
192
205
|
const exists = adapter.get("SELECT 1 FROM __lattice_migrations WHERE version = ?", [
|
|
193
|
-
|
|
206
|
+
versionStr
|
|
194
207
|
]);
|
|
195
208
|
if (!exists) {
|
|
196
209
|
adapter.run(m.sql);
|
|
197
210
|
adapter.run("INSERT INTO __lattice_migrations (version, applied_at) VALUES (?, ?)", [
|
|
198
|
-
|
|
211
|
+
versionStr,
|
|
199
212
|
(/* @__PURE__ */ new Date()).toISOString()
|
|
200
213
|
]);
|
|
201
214
|
}
|
|
@@ -230,6 +243,7 @@ var SchemaManager = class {
|
|
|
230
243
|
const existing = adapter.all(`PRAGMA table_info("${table}")`).map((r) => r.name);
|
|
231
244
|
for (const [col, type] of Object.entries(columns)) {
|
|
232
245
|
if (!existing.includes(col)) {
|
|
246
|
+
if (type.toUpperCase().includes("PRIMARY KEY")) continue;
|
|
233
247
|
adapter.run(`ALTER TABLE "${table}" ADD COLUMN "${col}" ${type}`);
|
|
234
248
|
}
|
|
235
249
|
}
|
|
@@ -1586,7 +1600,8 @@ var Lattice = class {
|
|
|
1586
1600
|
this._assertNotInit("define");
|
|
1587
1601
|
const compiledDef = {
|
|
1588
1602
|
...def,
|
|
1589
|
-
render: compileRender(def, table, this._schema, this._adapter)
|
|
1603
|
+
render: def.render ? compileRender(def, table, this._schema, this._adapter) : () => "",
|
|
1604
|
+
outputFile: def.outputFile ?? `.schema-only/${table}.md`
|
|
1590
1605
|
};
|
|
1591
1606
|
this._schema.define(table, compiledDef);
|
|
1592
1607
|
return this;
|
|
@@ -1628,6 +1643,23 @@ var Lattice = class {
|
|
|
1628
1643
|
this._initialized = true;
|
|
1629
1644
|
return Promise.resolve();
|
|
1630
1645
|
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Run additional migrations after init(). Useful for package-level schema
|
|
1648
|
+
* changes applied at runtime (e.g. update hooks that add columns).
|
|
1649
|
+
*
|
|
1650
|
+
* @since 0.17.0
|
|
1651
|
+
*/
|
|
1652
|
+
migrate(migrations) {
|
|
1653
|
+
if (!this._initialized) {
|
|
1654
|
+
return Promise.reject(new Error("Lattice: not initialized \u2014 call init() first"));
|
|
1655
|
+
}
|
|
1656
|
+
this._schema.applyMigrations(this._adapter, migrations);
|
|
1657
|
+
for (const tableName of this._schema.getTables().keys()) {
|
|
1658
|
+
const rows = this._adapter.all(`PRAGMA table_info("${tableName}")`);
|
|
1659
|
+
this._columnCache.set(tableName, new Set(rows.map((r) => r.name)));
|
|
1660
|
+
}
|
|
1661
|
+
return Promise.resolve();
|
|
1662
|
+
}
|
|
1631
1663
|
close() {
|
|
1632
1664
|
this._adapter.close();
|
|
1633
1665
|
this._columnCache.clear();
|
|
@@ -1660,6 +1692,17 @@ var Lattice = class {
|
|
|
1660
1692
|
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
1661
1693
|
return Promise.resolve(pkValue);
|
|
1662
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Insert a row and return the full inserted row (including auto-generated
|
|
1697
|
+
* fields and defaults). Equivalent to `insert()` followed by `get()`.
|
|
1698
|
+
*
|
|
1699
|
+
* @since 0.17.0
|
|
1700
|
+
*/
|
|
1701
|
+
insertReturning(table, row) {
|
|
1702
|
+
return this.insert(table, row).then(
|
|
1703
|
+
(pk) => this.get(table, pk).then((result) => result ?? { ...row, id: pk })
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1663
1706
|
upsert(table, row) {
|
|
1664
1707
|
const notInit = this._notInitError();
|
|
1665
1708
|
if (notInit) return notInit;
|
|
@@ -1714,6 +1757,17 @@ var Lattice = class {
|
|
|
1714
1757
|
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
1715
1758
|
return Promise.resolve();
|
|
1716
1759
|
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Update a row and return the full updated row. Equivalent to `update()`
|
|
1762
|
+
* followed by `get()`.
|
|
1763
|
+
*
|
|
1764
|
+
* @since 0.17.0
|
|
1765
|
+
*/
|
|
1766
|
+
updateReturning(table, id, row) {
|
|
1767
|
+
return this.update(table, id, row).then(
|
|
1768
|
+
() => this.get(table, id).then((result) => result ?? row)
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1717
1771
|
delete(table, id) {
|
|
1718
1772
|
const notInit = this._notInitError();
|
|
1719
1773
|
if (notInit) return notInit;
|