latticesql 0.16.0 → 0.16.2
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 +95 -62
- package/dist/cli.js +80 -29
- package/dist/index.cjs +106 -37
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +106 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -376,15 +376,21 @@ db.defineEntityContext('agents', {
|
|
|
376
376
|
// Files inside each entity's directory
|
|
377
377
|
files: {
|
|
378
378
|
'AGENT.md': {
|
|
379
|
-
source: { type: 'self' },
|
|
380
|
-
render: ([r]) => `# ${r.name as string}\n\n${r.bio as string ?? ''}`,
|
|
379
|
+
source: { type: 'self' }, // entity's own row
|
|
380
|
+
render: ([r]) => `# ${r.name as string}\n\n${(r.bio as string) ?? ''}`,
|
|
381
381
|
},
|
|
382
382
|
'TASKS.md': {
|
|
383
|
-
source: {
|
|
384
|
-
|
|
383
|
+
source: {
|
|
384
|
+
type: 'hasMany',
|
|
385
|
+
table: 'tasks',
|
|
386
|
+
foreignKey: 'agent_id',
|
|
387
|
+
orderBy: 'created_at',
|
|
388
|
+
orderDir: 'desc',
|
|
389
|
+
limit: 20,
|
|
390
|
+
},
|
|
385
391
|
render: (rows) => rows.map((r) => `- ${r.title as string}`).join('\n'),
|
|
386
|
-
omitIfEmpty: true,
|
|
387
|
-
budget: 4000,
|
|
392
|
+
omitIfEmpty: true, // skip if no tasks
|
|
393
|
+
budget: 4000, // truncate at 4 000 chars
|
|
388
394
|
},
|
|
389
395
|
'SKILLS.md': {
|
|
390
396
|
source: {
|
|
@@ -393,7 +399,7 @@ db.defineEntityContext('agents', {
|
|
|
393
399
|
localKey: 'agent_id',
|
|
394
400
|
remoteKey: 'skill_id',
|
|
395
401
|
remoteTable: 'skills',
|
|
396
|
-
orderBy: 'name',
|
|
402
|
+
orderBy: 'name', // softDelete inherited from sourceDefaults
|
|
397
403
|
},
|
|
398
404
|
render: (rows) => rows.map((r) => `- ${r.name as string}`).join('\n'),
|
|
399
405
|
omitIfEmpty: true,
|
|
@@ -426,14 +432,14 @@ context/
|
|
|
426
432
|
|
|
427
433
|
**Source types:**
|
|
428
434
|
|
|
429
|
-
| Type
|
|
430
|
-
|
|
431
|
-
| `{ type: 'self' }`
|
|
432
|
-
| `{ type: 'hasMany', table, foreignKey, ... }`
|
|
433
|
-
| `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable, ... }` | Remote rows via a junction table
|
|
434
|
-
| `{ type: 'belongsTo', table, foreignKey, ... }`
|
|
435
|
-
| `{ type: 'enriched', include: { ... } }`
|
|
436
|
-
| `{ type: 'custom', query: (row, adapter) => Row[] }`
|
|
435
|
+
| Type | What it queries |
|
|
436
|
+
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------- |
|
|
437
|
+
| `{ type: 'self' }` | The entity row itself |
|
|
438
|
+
| `{ type: 'hasMany', table, foreignKey, ... }` | Rows in `table` where `foreignKey = entityPk` |
|
|
439
|
+
| `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable, ... }` | Remote rows via a junction table |
|
|
440
|
+
| `{ type: 'belongsTo', table, foreignKey, ... }` | Single parent row via FK on this entity (`null` FK → empty) |
|
|
441
|
+
| `{ type: 'enriched', include: { ... } }` | Entity row + related data attached as `_key` JSON fields (v0.7+) |
|
|
442
|
+
| `{ type: 'custom', query: (row, adapter) => Row[] }` | Fully custom synchronous query |
|
|
437
443
|
|
|
438
444
|
#### Source query options (v0.6+)
|
|
439
445
|
|
|
@@ -502,7 +508,7 @@ Set default query options for all relationship sources in an entity context:
|
|
|
502
508
|
```typescript
|
|
503
509
|
db.defineEntityContext('agents', {
|
|
504
510
|
slug: (row) => row.slug as string,
|
|
505
|
-
sourceDefaults: { softDelete: true },
|
|
511
|
+
sourceDefaults: { softDelete: true }, // applied to all hasMany/manyToMany/belongsTo
|
|
506
512
|
files: {
|
|
507
513
|
'TASKS.md': {
|
|
508
514
|
// softDelete: true is inherited from sourceDefaults
|
|
@@ -548,6 +554,7 @@ Starts with the entity's own row and attaches related data as JSON string fields
|
|
|
548
554
|
`EntityFileSpec.render` accepts declarative template objects in addition to functions. Three built-in templates:
|
|
549
555
|
|
|
550
556
|
**entity-table** — heading + GFM table:
|
|
557
|
+
|
|
551
558
|
```typescript
|
|
552
559
|
render: {
|
|
553
560
|
template: 'entity-table',
|
|
@@ -562,6 +569,7 @@ render: {
|
|
|
562
569
|
```
|
|
563
570
|
|
|
564
571
|
**entity-profile** — heading + field-value pairs + enriched JSON sections:
|
|
572
|
+
|
|
565
573
|
```typescript
|
|
566
574
|
render: {
|
|
567
575
|
template: 'entity-profile',
|
|
@@ -581,6 +589,7 @@ render: {
|
|
|
581
589
|
```
|
|
582
590
|
|
|
583
591
|
**entity-sections** — per-row sections with metadata + body:
|
|
592
|
+
|
|
584
593
|
```typescript
|
|
585
594
|
render: {
|
|
586
595
|
template: 'entity-sections',
|
|
@@ -615,7 +624,7 @@ Register a post-write lifecycle hook that fires after `insert()`, `update()`, or
|
|
|
615
624
|
db.defineWriteHook({
|
|
616
625
|
table: 'agents',
|
|
617
626
|
on: ['insert', 'update'],
|
|
618
|
-
watchColumns: ['team_id', 'division'],
|
|
627
|
+
watchColumns: ['team_id', 'division'], // only fire when these change
|
|
619
628
|
handler: (ctx) => {
|
|
620
629
|
// ctx.table, ctx.op, ctx.row, ctx.pk, ctx.changedColumns
|
|
621
630
|
console.log(`${ctx.op} on ${ctx.table}: ${ctx.pk}`);
|
|
@@ -626,12 +635,12 @@ db.defineWriteHook({
|
|
|
626
635
|
|
|
627
636
|
**Options:**
|
|
628
637
|
|
|
629
|
-
| Field
|
|
630
|
-
|
|
631
|
-
| `table`
|
|
632
|
-
| `on`
|
|
633
|
-
| `watchColumns` | `string[]` (optional)
|
|
634
|
-
| `handler`
|
|
638
|
+
| Field | Type | Description |
|
|
639
|
+
| -------------- | ----------------------------------------- | ---------------------------------------------- |
|
|
640
|
+
| `table` | `string` | Table to watch |
|
|
641
|
+
| `on` | `Array<'insert' \| 'update' \| 'delete'>` | Operations that trigger the hook |
|
|
642
|
+
| `watchColumns` | `string[]` (optional) | Only fire on update when these columns changed |
|
|
643
|
+
| `handler` | `(ctx: WriteHookContext) => void` | Synchronous handler |
|
|
635
644
|
|
|
636
645
|
Hook errors are caught and routed to error handlers — they never crash the caller. Multiple hooks per table are supported.
|
|
637
646
|
|
|
@@ -677,9 +686,16 @@ Methods that work on **any table** — including tables created via raw DDL (not
|
|
|
677
686
|
|
|
678
687
|
```typescript
|
|
679
688
|
// Upsert by natural key (not just UUID). Auto-handles org_id, updated_at, deleted_at.
|
|
680
|
-
const id = await db.upsertByNaturalKey(
|
|
681
|
-
|
|
682
|
-
|
|
689
|
+
const id = await db.upsertByNaturalKey(
|
|
690
|
+
'agents',
|
|
691
|
+
'name',
|
|
692
|
+
'Alice',
|
|
693
|
+
{
|
|
694
|
+
role: 'engineer',
|
|
695
|
+
status: 'active',
|
|
696
|
+
},
|
|
697
|
+
{ sourceFile: 'agents.md', orgId: 'org-1' },
|
|
698
|
+
);
|
|
683
699
|
|
|
684
700
|
// Sparse update — only writes non-null fields.
|
|
685
701
|
await db.enrichByNaturalKey('agents', 'name', 'Alice', { title: 'Senior Engineer' });
|
|
@@ -700,7 +716,11 @@ const alice = await db.getByNaturalKey('agents', 'name', 'Alice');
|
|
|
700
716
|
await db.link('agent_skills', { agent_id: 'a1', skill_id: 's1', proficiency: 'expert' });
|
|
701
717
|
|
|
702
718
|
// Link with upsert (INSERT OR REPLACE — updates existing)
|
|
703
|
-
await db.link(
|
|
719
|
+
await db.link(
|
|
720
|
+
'agent_projects',
|
|
721
|
+
{ agent_id: 'a1', project_id: 'p1', role: 'lead' },
|
|
722
|
+
{ upsert: true },
|
|
723
|
+
);
|
|
704
724
|
|
|
705
725
|
// Unlink (DELETE matching rows)
|
|
706
726
|
await db.unlink('agent_projects', { agent_id: 'a1', project_id: 'p1' });
|
|
@@ -748,11 +768,19 @@ Declarative report builder — queries data within a time window, groups into se
|
|
|
748
768
|
|
|
749
769
|
```typescript
|
|
750
770
|
const report = await db.buildReport({
|
|
751
|
-
since: '8h',
|
|
771
|
+
since: '8h', // or '24h', '7d', or ISO timestamp
|
|
752
772
|
sections: [
|
|
753
|
-
{
|
|
773
|
+
{
|
|
774
|
+
name: 'tasks',
|
|
775
|
+
query: { table: 'tasks', orderBy: 'created_at', orderDir: 'desc' },
|
|
776
|
+
format: 'count_and_list',
|
|
777
|
+
},
|
|
754
778
|
{ name: 'events', query: { table: 'activity', groupBy: 'type' }, format: 'counts' },
|
|
755
|
-
{
|
|
779
|
+
{
|
|
780
|
+
name: 'alerts',
|
|
781
|
+
query: { table: 'activity', filters: [{ col: 'severity', op: 'lte', val: 2 }] },
|
|
782
|
+
format: 'list',
|
|
783
|
+
},
|
|
756
784
|
],
|
|
757
785
|
});
|
|
758
786
|
|
|
@@ -771,11 +799,13 @@ import { createSQLiteStateStore } from 'latticesql';
|
|
|
771
799
|
|
|
772
800
|
db.defineWriteback({
|
|
773
801
|
file: './agents/*/SESSION.md',
|
|
774
|
-
stateStore: createSQLiteStateStore(db.db),
|
|
802
|
+
stateStore: createSQLiteStateStore(db.db), // persists offsets in SQLite
|
|
775
803
|
parse: (content, offset) => myParser(content, offset),
|
|
776
|
-
persist: async (entry, filePath) => {
|
|
804
|
+
persist: async (entry, filePath) => {
|
|
805
|
+
/* ... */
|
|
806
|
+
},
|
|
777
807
|
dedupeKey: (entry) => entry.id,
|
|
778
|
-
onArchive: (filePath) => archiveFile(filePath),
|
|
808
|
+
onArchive: (filePath) => archiveFile(filePath), // lifecycle hook
|
|
779
809
|
});
|
|
780
810
|
```
|
|
781
811
|
|
|
@@ -1046,9 +1076,9 @@ stop();
|
|
|
1046
1076
|
const stop = await db.watch('./context', {
|
|
1047
1077
|
interval: 10_000,
|
|
1048
1078
|
cleanup: {
|
|
1049
|
-
removeOrphanedDirectories: true,
|
|
1050
|
-
removeOrphanedFiles: true,
|
|
1051
|
-
protectedFiles: ['SESSION.md'],
|
|
1079
|
+
removeOrphanedDirectories: true, // delete dirs for deleted entities
|
|
1080
|
+
removeOrphanedFiles: true, // delete stale relationship files
|
|
1081
|
+
protectedFiles: ['SESSION.md'], // never delete these
|
|
1052
1082
|
dryRun: false,
|
|
1053
1083
|
},
|
|
1054
1084
|
onCleanup: (r) => {
|
|
@@ -1074,15 +1104,15 @@ const result = await db.reconcile('./context', {
|
|
|
1074
1104
|
removeOrphanedDirectories: true,
|
|
1075
1105
|
removeOrphanedFiles: true,
|
|
1076
1106
|
protectedFiles: ['SESSION.md'],
|
|
1077
|
-
reverseSync: true,
|
|
1078
|
-
dryRun: false,
|
|
1107
|
+
reverseSync: true, // default; set false to skip, 'dry-run' to preview
|
|
1108
|
+
dryRun: false, // set true to preview without deleting
|
|
1079
1109
|
onOrphan: (path, kind) => console.log(`would remove ${kind}: ${path}`),
|
|
1080
1110
|
});
|
|
1081
1111
|
|
|
1082
|
-
console.log(result.filesWritten);
|
|
1083
|
-
console.log(result.cleanup.directoriesRemoved);
|
|
1084
|
-
console.log(result.cleanup.warnings);
|
|
1085
|
-
console.log(result.reverseSync);
|
|
1112
|
+
console.log(result.filesWritten); // files written this cycle
|
|
1113
|
+
console.log(result.cleanup.directoriesRemoved); // orphaned dirs removed
|
|
1114
|
+
console.log(result.cleanup.warnings); // dirs left in place (user files)
|
|
1115
|
+
console.log(result.reverseSync); // { filesScanned, filesChanged, updatesApplied, errors }
|
|
1086
1116
|
```
|
|
1087
1117
|
|
|
1088
1118
|
`ReconcileResult` extends `RenderResult` with `cleanup` and `reverseSync` fields:
|
|
@@ -1278,9 +1308,9 @@ Generate a GitHub-Flavoured Markdown table from rows with explicit column config
|
|
|
1278
1308
|
import { markdownTable } from 'latticesql';
|
|
1279
1309
|
|
|
1280
1310
|
const md = markdownTable(rows, [
|
|
1281
|
-
{ key: 'name',
|
|
1311
|
+
{ key: 'name', header: 'Name' },
|
|
1282
1312
|
{ key: 'status', header: 'Status', format: (v) => String(v || '—') },
|
|
1283
|
-
{ key: 'name',
|
|
1313
|
+
{ key: 'name', header: 'Detail', format: (v, row) => `[view](${row.slug}/DETAIL.md)` },
|
|
1284
1314
|
]);
|
|
1285
1315
|
// | Name | Status | Detail |
|
|
1286
1316
|
// | --- | --- | --- |
|
|
@@ -1296,8 +1326,8 @@ Generate a URL-safe slug from a display name — lowercases, strips diacritics,
|
|
|
1296
1326
|
```typescript
|
|
1297
1327
|
import { slugify } from 'latticesql';
|
|
1298
1328
|
|
|
1299
|
-
slugify('My Agent Name');
|
|
1300
|
-
slugify('Jose Garcia');
|
|
1329
|
+
slugify('My Agent Name'); // 'my-agent-name'
|
|
1330
|
+
slugify('Jose Garcia'); // 'jose-garcia'
|
|
1301
1331
|
```
|
|
1302
1332
|
|
|
1303
1333
|
### `truncate(content, maxChars, notice?)`
|
|
@@ -1332,7 +1362,7 @@ db.defineEntityContext('projects', {
|
|
|
1332
1362
|
files: {
|
|
1333
1363
|
'PROJECT.md': {
|
|
1334
1364
|
source: { type: 'self' },
|
|
1335
|
-
render: ([r]) => `# ${r.name as string}\n\n${r.description as string ?? ''}`,
|
|
1365
|
+
render: ([r]) => `# ${r.name as string}\n\n${(r.description as string) ?? ''}`,
|
|
1336
1366
|
},
|
|
1337
1367
|
},
|
|
1338
1368
|
});
|
|
@@ -1358,7 +1388,7 @@ await db.delete('projects', 'old-id');
|
|
|
1358
1388
|
|
|
1359
1389
|
const result = await db.reconcile('./ctx', {
|
|
1360
1390
|
removeOrphanedDirectories: true,
|
|
1361
|
-
protectedFiles: ['NOTES.md'],
|
|
1391
|
+
protectedFiles: ['NOTES.md'], // agents wrote these — keep them
|
|
1362
1392
|
});
|
|
1363
1393
|
// result.cleanup.directoriesRemoved → ['/.../ctx/projects/old-project']
|
|
1364
1394
|
```
|
|
@@ -1373,7 +1403,9 @@ Declare files that agents write inside entity directories. Lattice will never de
|
|
|
1373
1403
|
db.defineEntityContext('agents', {
|
|
1374
1404
|
slug: (r) => r.slug as string,
|
|
1375
1405
|
protectedFiles: ['SESSION.md', 'NOTES.md'],
|
|
1376
|
-
files: {
|
|
1406
|
+
files: {
|
|
1407
|
+
/* ... */
|
|
1408
|
+
},
|
|
1377
1409
|
});
|
|
1378
1410
|
```
|
|
1379
1411
|
|
|
@@ -1417,18 +1449,18 @@ target: agent-id-here
|
|
|
1417
1449
|
reason: Updating status after deployment completed.
|
|
1418
1450
|
---
|
|
1419
1451
|
status: active
|
|
1420
|
-
last_task:
|
|
1452
|
+
last_task: api-deploy
|
|
1421
1453
|
===
|
|
1422
1454
|
```
|
|
1423
1455
|
|
|
1424
|
-
| Header
|
|
1425
|
-
|
|
1426
|
-
| `type`
|
|
1427
|
-
| `timestamp` | Yes
|
|
1428
|
-
| `op`
|
|
1429
|
-
| `table`
|
|
1430
|
-
| `target`
|
|
1431
|
-
| `reason`
|
|
1456
|
+
| Header | Required | Description |
|
|
1457
|
+
| ----------- | ----------------- | ----------------------------------- |
|
|
1458
|
+
| `type` | Yes | Must be `write` |
|
|
1459
|
+
| `timestamp` | Yes | ISO 8601 |
|
|
1460
|
+
| `op` | Yes | `create`, `update`, or `delete` |
|
|
1461
|
+
| `table` | Yes | Target table name |
|
|
1462
|
+
| `target` | For update/delete | Record primary key |
|
|
1463
|
+
| `reason` | Encouraged | Human-readable reason (audit trail) |
|
|
1432
1464
|
|
|
1433
1465
|
**Body**: `key: value` pairs — one field per line. Field names are validated against the table schema before any write is applied.
|
|
1434
1466
|
|
|
@@ -1449,15 +1481,16 @@ for (const entry of result.entries) {
|
|
|
1449
1481
|
```
|
|
1450
1482
|
|
|
1451
1483
|
**`SessionWriteEntry`:**
|
|
1484
|
+
|
|
1452
1485
|
```ts
|
|
1453
1486
|
interface SessionWriteEntry {
|
|
1454
|
-
id: string;
|
|
1455
|
-
timestamp: string;
|
|
1487
|
+
id: string; // content-addressed ID
|
|
1488
|
+
timestamp: string; // ISO 8601
|
|
1456
1489
|
op: 'create' | 'update' | 'delete';
|
|
1457
1490
|
table: string;
|
|
1458
|
-
target?: string;
|
|
1491
|
+
target?: string; // required for update/delete
|
|
1459
1492
|
reason?: string;
|
|
1460
|
-
fields: Record<string, string>;
|
|
1493
|
+
fields: Record<string, string>; // empty for delete
|
|
1461
1494
|
}
|
|
1462
1495
|
```
|
|
1463
1496
|
|
package/dist/cli.js
CHANGED
|
@@ -47,13 +47,13 @@ function buildParsedConfig(raw, sourceName, configDir) {
|
|
|
47
47
|
const dbPath = resolve(configDir, config.db);
|
|
48
48
|
const tables = [];
|
|
49
49
|
for (const [entityName, entityDef] of Object.entries(config.entities)) {
|
|
50
|
-
const definition = entityToTableDef(entityName, entityDef
|
|
50
|
+
const definition = entityToTableDef(entityName, entityDef);
|
|
51
51
|
tables.push({ name: entityName, definition });
|
|
52
52
|
}
|
|
53
53
|
const entityContexts = parseEntityContexts(config.entityContexts);
|
|
54
54
|
return { dbPath, tables, entityContexts };
|
|
55
55
|
}
|
|
56
|
-
function entityToTableDef(entityName, entity
|
|
56
|
+
function entityToTableDef(entityName, entity) {
|
|
57
57
|
const rawFields = entity.fields;
|
|
58
58
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
59
59
|
throw new Error(`Lattice: entity "${entityName}" must have a "fields" object`);
|
|
@@ -77,7 +77,7 @@ function entityToTableDef(entityName, entity, configDir) {
|
|
|
77
77
|
}
|
|
78
78
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
79
79
|
const render = parseEntityRender(entity.render);
|
|
80
|
-
const outputFile =
|
|
80
|
+
const outputFile = entity.outputFile;
|
|
81
81
|
return {
|
|
82
82
|
columns,
|
|
83
83
|
render,
|
|
@@ -528,7 +528,9 @@ var SchemaManager = class {
|
|
|
528
528
|
if (this._entityContexts.has(name)) {
|
|
529
529
|
const cols = adapter.all(`PRAGMA table_info("${name}")`);
|
|
530
530
|
const hasDeletedAt = cols.some((c) => c.name === "deleted_at");
|
|
531
|
-
return adapter.all(
|
|
531
|
+
return adapter.all(
|
|
532
|
+
`SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`
|
|
533
|
+
);
|
|
532
534
|
}
|
|
533
535
|
throw new Error(`Unknown table: "${name}"`);
|
|
534
536
|
}
|
|
@@ -1100,7 +1102,14 @@ var RenderEngine = class {
|
|
|
1100
1102
|
const slugs = new Set(rows.map((row) => def.slug(row)));
|
|
1101
1103
|
currentSlugsByTable.set(table, slugs);
|
|
1102
1104
|
}
|
|
1103
|
-
return cleanupEntityContexts(
|
|
1105
|
+
return cleanupEntityContexts(
|
|
1106
|
+
outputDir,
|
|
1107
|
+
entityContexts,
|
|
1108
|
+
currentSlugsByTable,
|
|
1109
|
+
prevManifest,
|
|
1110
|
+
options,
|
|
1111
|
+
newManifest
|
|
1112
|
+
);
|
|
1104
1113
|
}
|
|
1105
1114
|
/**
|
|
1106
1115
|
* Render all entity context definitions.
|
|
@@ -1215,7 +1224,6 @@ var ReverseSyncEngine = class {
|
|
|
1215
1224
|
}
|
|
1216
1225
|
}
|
|
1217
1226
|
if (reverseSyncFiles.size === 0) continue;
|
|
1218
|
-
const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
1219
1227
|
const allRows = this._schema.queryTable(this._adapter, table);
|
|
1220
1228
|
const slugToRow = /* @__PURE__ */ new Map();
|
|
1221
1229
|
for (const row of allRows) {
|
|
@@ -1247,7 +1255,7 @@ var ReverseSyncEngine = class {
|
|
|
1247
1255
|
const updates = reverseSyncFn(currentContent, entityRow);
|
|
1248
1256
|
if (updates.length === 0) continue;
|
|
1249
1257
|
if (!dryRun) {
|
|
1250
|
-
this._applyUpdates(updates
|
|
1258
|
+
this._applyUpdates(updates);
|
|
1251
1259
|
}
|
|
1252
1260
|
result.updatesApplied += updates.length;
|
|
1253
1261
|
} catch (err) {
|
|
@@ -1266,7 +1274,7 @@ var ReverseSyncEngine = class {
|
|
|
1266
1274
|
* Each update is an independent UPDATE statement.
|
|
1267
1275
|
* Wrapped in a transaction for atomicity.
|
|
1268
1276
|
*/
|
|
1269
|
-
_applyUpdates(updates
|
|
1277
|
+
_applyUpdates(updates) {
|
|
1270
1278
|
this._adapter.run("BEGIN");
|
|
1271
1279
|
try {
|
|
1272
1280
|
for (const update of updates) {
|
|
@@ -1283,10 +1291,7 @@ var ReverseSyncEngine = class {
|
|
|
1283
1291
|
const setClause = setCols.map((c) => `"${c}" = ?`).join(", ");
|
|
1284
1292
|
const whereClause = pkCols.map((c) => `"${c}" = ?`).join(" AND ");
|
|
1285
1293
|
const sql = `UPDATE "${update.table}" SET ${setClause} WHERE ${whereClause}`;
|
|
1286
|
-
const params = [
|
|
1287
|
-
...setCols.map((c) => update.set[c]),
|
|
1288
|
-
...pkCols.map((c) => update.pk[c])
|
|
1289
|
-
];
|
|
1294
|
+
const params = [...setCols.map((c) => update.set[c]), ...pkCols.map((c) => update.pk[c])];
|
|
1290
1295
|
this._adapter.run(sql, params);
|
|
1291
1296
|
}
|
|
1292
1297
|
this._adapter.run("COMMIT");
|
|
@@ -1356,8 +1361,12 @@ var InMemoryStateStore = class {
|
|
|
1356
1361
|
return this._seen.get(filePath)?.has(key) ?? false;
|
|
1357
1362
|
}
|
|
1358
1363
|
markSeen(filePath, key) {
|
|
1359
|
-
|
|
1360
|
-
|
|
1364
|
+
let seenSet = this._seen.get(filePath);
|
|
1365
|
+
if (!seenSet) {
|
|
1366
|
+
seenSet = /* @__PURE__ */ new Set();
|
|
1367
|
+
this._seen.set(filePath, seenSet);
|
|
1368
|
+
}
|
|
1369
|
+
seenSet.add(key);
|
|
1361
1370
|
}
|
|
1362
1371
|
};
|
|
1363
1372
|
|
|
@@ -1774,19 +1783,32 @@ var Lattice = class {
|
|
|
1774
1783
|
const entries = Object.entries(withConventions).filter(([k]) => k !== "id");
|
|
1775
1784
|
if (entries.length === 0) return Promise.resolve(existing.id);
|
|
1776
1785
|
const setCols = entries.map(([k]) => `"${k}" = ?`).join(", ");
|
|
1777
|
-
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1778
|
-
|
|
1786
|
+
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1787
|
+
...entries.map(([, v]) => v),
|
|
1788
|
+
existing.id
|
|
1789
|
+
]);
|
|
1790
|
+
this._fireWriteHooks(
|
|
1791
|
+
table,
|
|
1792
|
+
"update",
|
|
1793
|
+
withConventions,
|
|
1794
|
+
existing.id,
|
|
1795
|
+
Object.keys(sanitized)
|
|
1796
|
+
);
|
|
1779
1797
|
return Promise.resolve(existing.id);
|
|
1780
1798
|
}
|
|
1781
1799
|
const id = sanitized.id ?? uuidv4();
|
|
1782
1800
|
const insertData = { ...withConventions, id, [naturalKeyCol]: naturalKeyVal };
|
|
1783
1801
|
if (opts?.orgId && cols.has("org_id") && !insertData.org_id) insertData.org_id = opts.orgId;
|
|
1784
1802
|
if (cols.has("deleted_at")) insertData.deleted_at = null;
|
|
1785
|
-
if (cols.has("created_at") && !insertData.created_at)
|
|
1803
|
+
if (cols.has("created_at") && !insertData.created_at)
|
|
1804
|
+
insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1786
1805
|
const filtered = this._filterToSchemaColumns(table, insertData);
|
|
1787
1806
|
const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
|
|
1788
1807
|
const placeholders = Object.keys(filtered).map(() => "?").join(", ");
|
|
1789
|
-
this._adapter.run(
|
|
1808
|
+
this._adapter.run(
|
|
1809
|
+
`INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`,
|
|
1810
|
+
Object.values(filtered)
|
|
1811
|
+
);
|
|
1790
1812
|
this._fireWriteHooks(table, "insert", filtered, id);
|
|
1791
1813
|
return Promise.resolve(id);
|
|
1792
1814
|
}
|
|
@@ -1803,14 +1825,25 @@ var Lattice = class {
|
|
|
1803
1825
|
);
|
|
1804
1826
|
if (!existing) return Promise.resolve(false);
|
|
1805
1827
|
const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(data));
|
|
1806
|
-
const entries = Object.entries(sanitized).filter(
|
|
1828
|
+
const entries = Object.entries(sanitized).filter(
|
|
1829
|
+
([k, v]) => v !== null && v !== void 0 && k !== "id"
|
|
1830
|
+
);
|
|
1807
1831
|
if (entries.length === 0) return Promise.resolve(true);
|
|
1808
1832
|
const cols = this._ensureColumnCache(table);
|
|
1809
1833
|
const withTs = [...entries];
|
|
1810
1834
|
if (cols.has("updated_at")) withTs.push(["updated_at", (/* @__PURE__ */ new Date()).toISOString()]);
|
|
1811
1835
|
const setCols = withTs.map(([k]) => `"${k}" = ?`).join(", ");
|
|
1812
|
-
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1813
|
-
|
|
1836
|
+
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1837
|
+
...withTs.map(([, v]) => v),
|
|
1838
|
+
existing.id
|
|
1839
|
+
]);
|
|
1840
|
+
this._fireWriteHooks(
|
|
1841
|
+
table,
|
|
1842
|
+
"update",
|
|
1843
|
+
Object.fromEntries(entries),
|
|
1844
|
+
existing.id,
|
|
1845
|
+
entries.map(([k]) => k)
|
|
1846
|
+
);
|
|
1814
1847
|
return Promise.resolve(true);
|
|
1815
1848
|
}
|
|
1816
1849
|
/**
|
|
@@ -1888,7 +1921,10 @@ var Lattice = class {
|
|
|
1888
1921
|
const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
|
|
1889
1922
|
const placeholders = Object.keys(filtered).map(() => "?").join(", ");
|
|
1890
1923
|
const verb = opts?.upsert ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
|
|
1891
|
-
this._adapter.run(
|
|
1924
|
+
this._adapter.run(
|
|
1925
|
+
`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
|
|
1926
|
+
Object.values(filtered)
|
|
1927
|
+
);
|
|
1892
1928
|
return Promise.resolve();
|
|
1893
1929
|
}
|
|
1894
1930
|
/**
|
|
@@ -1900,7 +1936,10 @@ var Lattice = class {
|
|
|
1900
1936
|
const entries = Object.entries(conditions);
|
|
1901
1937
|
if (entries.length === 0) return Promise.resolve();
|
|
1902
1938
|
const where = entries.map(([k]) => `"${k}" = ?`).join(" AND ");
|
|
1903
|
-
this._adapter.run(
|
|
1939
|
+
this._adapter.run(
|
|
1940
|
+
`DELETE FROM "${junctionTable}" WHERE ${where}`,
|
|
1941
|
+
entries.map(([, v]) => v)
|
|
1942
|
+
);
|
|
1904
1943
|
return Promise.resolve();
|
|
1905
1944
|
}
|
|
1906
1945
|
// -------------------------------------------------------------------------
|
|
@@ -1927,7 +1966,13 @@ var Lattice = class {
|
|
|
1927
1966
|
if (config.sourceFile) upsertOpts.sourceFile = config.sourceFile;
|
|
1928
1967
|
if (config.sourceHash) upsertOpts.sourceHash = config.sourceHash;
|
|
1929
1968
|
if (config.orgId) upsertOpts.orgId = config.orgId;
|
|
1930
|
-
await this.upsertByNaturalKey(
|
|
1969
|
+
await this.upsertByNaturalKey(
|
|
1970
|
+
config.table,
|
|
1971
|
+
config.naturalKey,
|
|
1972
|
+
naturalKeyVal,
|
|
1973
|
+
record,
|
|
1974
|
+
upsertOpts
|
|
1975
|
+
);
|
|
1931
1976
|
upserted++;
|
|
1932
1977
|
if (config.linkTo) {
|
|
1933
1978
|
const recordId = await this.getByNaturalKey(config.table, config.naturalKey, naturalKeyVal);
|
|
@@ -1952,7 +1997,12 @@ var Lattice = class {
|
|
|
1952
1997
|
}
|
|
1953
1998
|
}
|
|
1954
1999
|
if (config.softDeleteMissing && config.sourceFile && keys.length > 0) {
|
|
1955
|
-
softDeleted = await this.softDeleteMissing(
|
|
2000
|
+
softDeleted = await this.softDeleteMissing(
|
|
2001
|
+
config.table,
|
|
2002
|
+
config.naturalKey,
|
|
2003
|
+
config.sourceFile,
|
|
2004
|
+
keys
|
|
2005
|
+
);
|
|
1956
2006
|
}
|
|
1957
2007
|
return { upserted, linked, softDeleted };
|
|
1958
2008
|
}
|
|
@@ -2036,7 +2086,10 @@ var Lattice = class {
|
|
|
2036
2086
|
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
2037
2087
|
const orderBy = section.query.orderBy ? ` ORDER BY "${section.query.orderBy}" ${section.query.orderDir === "desc" ? "DESC" : "ASC"}` : "";
|
|
2038
2088
|
const limit = section.query.limit ? ` LIMIT ${String(section.query.limit)}` : "";
|
|
2039
|
-
const rows = this._adapter.all(
|
|
2089
|
+
const rows = this._adapter.all(
|
|
2090
|
+
`SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`,
|
|
2091
|
+
params
|
|
2092
|
+
);
|
|
2040
2093
|
if (rows.length > 0) allEmpty = false;
|
|
2041
2094
|
let formatted = "";
|
|
2042
2095
|
if (section.format === "custom" && section.customFormat) {
|
|
@@ -2359,9 +2412,7 @@ var Lattice = class {
|
|
|
2359
2412
|
if (!known) return null;
|
|
2360
2413
|
for (const col of cols) {
|
|
2361
2414
|
if (!known.has(col)) {
|
|
2362
|
-
return Promise.reject(
|
|
2363
|
-
new Error(`Lattice: unknown column "${col}" in table "${table}"`)
|
|
2364
|
-
);
|
|
2415
|
+
return Promise.reject(new Error(`Lattice: unknown column "${col}" in table "${table}"`));
|
|
2365
2416
|
}
|
|
2366
2417
|
}
|
|
2367
2418
|
return null;
|
package/dist/index.cjs
CHANGED
|
@@ -276,7 +276,9 @@ var SchemaManager = class {
|
|
|
276
276
|
if (this._entityContexts.has(name)) {
|
|
277
277
|
const cols = adapter.all(`PRAGMA table_info("${name}")`);
|
|
278
278
|
const hasDeletedAt = cols.some((c) => c.name === "deleted_at");
|
|
279
|
-
return adapter.all(
|
|
279
|
+
return adapter.all(
|
|
280
|
+
`SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`
|
|
281
|
+
);
|
|
280
282
|
}
|
|
281
283
|
throw new Error(`Unknown table: "${name}"`);
|
|
282
284
|
}
|
|
@@ -855,7 +857,14 @@ var RenderEngine = class {
|
|
|
855
857
|
const slugs = new Set(rows.map((row) => def.slug(row)));
|
|
856
858
|
currentSlugsByTable.set(table, slugs);
|
|
857
859
|
}
|
|
858
|
-
return cleanupEntityContexts(
|
|
860
|
+
return cleanupEntityContexts(
|
|
861
|
+
outputDir,
|
|
862
|
+
entityContexts,
|
|
863
|
+
currentSlugsByTable,
|
|
864
|
+
prevManifest,
|
|
865
|
+
options,
|
|
866
|
+
newManifest
|
|
867
|
+
);
|
|
859
868
|
}
|
|
860
869
|
/**
|
|
861
870
|
* Render all entity context definitions.
|
|
@@ -970,7 +979,6 @@ var ReverseSyncEngine = class {
|
|
|
970
979
|
}
|
|
971
980
|
}
|
|
972
981
|
if (reverseSyncFiles.size === 0) continue;
|
|
973
|
-
const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
974
982
|
const allRows = this._schema.queryTable(this._adapter, table);
|
|
975
983
|
const slugToRow = /* @__PURE__ */ new Map();
|
|
976
984
|
for (const row of allRows) {
|
|
@@ -1002,7 +1010,7 @@ var ReverseSyncEngine = class {
|
|
|
1002
1010
|
const updates = reverseSyncFn(currentContent, entityRow);
|
|
1003
1011
|
if (updates.length === 0) continue;
|
|
1004
1012
|
if (!dryRun) {
|
|
1005
|
-
this._applyUpdates(updates
|
|
1013
|
+
this._applyUpdates(updates);
|
|
1006
1014
|
}
|
|
1007
1015
|
result.updatesApplied += updates.length;
|
|
1008
1016
|
} catch (err) {
|
|
@@ -1021,7 +1029,7 @@ var ReverseSyncEngine = class {
|
|
|
1021
1029
|
* Each update is an independent UPDATE statement.
|
|
1022
1030
|
* Wrapped in a transaction for atomicity.
|
|
1023
1031
|
*/
|
|
1024
|
-
_applyUpdates(updates
|
|
1032
|
+
_applyUpdates(updates) {
|
|
1025
1033
|
this._adapter.run("BEGIN");
|
|
1026
1034
|
try {
|
|
1027
1035
|
for (const update of updates) {
|
|
@@ -1038,10 +1046,7 @@ var ReverseSyncEngine = class {
|
|
|
1038
1046
|
const setClause = setCols.map((c) => `"${c}" = ?`).join(", ");
|
|
1039
1047
|
const whereClause = pkCols.map((c) => `"${c}" = ?`).join(" AND ");
|
|
1040
1048
|
const sql = `UPDATE "${update.table}" SET ${setClause} WHERE ${whereClause}`;
|
|
1041
|
-
const params = [
|
|
1042
|
-
...setCols.map((c) => update.set[c]),
|
|
1043
|
-
...pkCols.map((c) => update.pk[c])
|
|
1044
|
-
];
|
|
1049
|
+
const params = [...setCols.map((c) => update.set[c]), ...pkCols.map((c) => update.pk[c])];
|
|
1045
1050
|
this._adapter.run(sql, params);
|
|
1046
1051
|
}
|
|
1047
1052
|
this._adapter.run("COMMIT");
|
|
@@ -1111,8 +1116,12 @@ var InMemoryStateStore = class {
|
|
|
1111
1116
|
return this._seen.get(filePath)?.has(key) ?? false;
|
|
1112
1117
|
}
|
|
1113
1118
|
markSeen(filePath, key) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1119
|
+
let seenSet = this._seen.get(filePath);
|
|
1120
|
+
if (!seenSet) {
|
|
1121
|
+
seenSet = /* @__PURE__ */ new Set();
|
|
1122
|
+
this._seen.set(filePath, seenSet);
|
|
1123
|
+
}
|
|
1124
|
+
seenSet.add(key);
|
|
1116
1125
|
}
|
|
1117
1126
|
};
|
|
1118
1127
|
var SQLiteStateStore = class {
|
|
@@ -1151,11 +1160,13 @@ var SQLiteStateStore = class {
|
|
|
1151
1160
|
}
|
|
1152
1161
|
setOffset(filePath, offset, size) {
|
|
1153
1162
|
this._init();
|
|
1154
|
-
this._db.prepare(
|
|
1163
|
+
this._db.prepare(
|
|
1164
|
+
`
|
|
1155
1165
|
INSERT INTO _lattice_writeback_offset (file_path, byte_offset, file_size, updated_at)
|
|
1156
1166
|
VALUES (?, ?, ?, datetime('now'))
|
|
1157
1167
|
ON CONFLICT(file_path) DO UPDATE SET byte_offset = ?, file_size = ?, updated_at = datetime('now')
|
|
1158
|
-
`
|
|
1168
|
+
`
|
|
1169
|
+
).run(filePath, offset, size, offset, size);
|
|
1159
1170
|
}
|
|
1160
1171
|
isSeen(filePath, key) {
|
|
1161
1172
|
this._init();
|
|
@@ -1396,13 +1407,13 @@ function buildParsedConfig(raw, sourceName, configDir) {
|
|
|
1396
1407
|
const dbPath = (0, import_node_path7.resolve)(configDir, config.db);
|
|
1397
1408
|
const tables = [];
|
|
1398
1409
|
for (const [entityName, entityDef] of Object.entries(config.entities)) {
|
|
1399
|
-
const definition = entityToTableDef(entityName, entityDef
|
|
1410
|
+
const definition = entityToTableDef(entityName, entityDef);
|
|
1400
1411
|
tables.push({ name: entityName, definition });
|
|
1401
1412
|
}
|
|
1402
1413
|
const entityContexts = parseEntityContexts(config.entityContexts);
|
|
1403
1414
|
return { dbPath, tables, entityContexts };
|
|
1404
1415
|
}
|
|
1405
|
-
function entityToTableDef(entityName, entity
|
|
1416
|
+
function entityToTableDef(entityName, entity) {
|
|
1406
1417
|
const rawFields = entity.fields;
|
|
1407
1418
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
1408
1419
|
throw new Error(`Lattice: entity "${entityName}" must have a "fields" object`);
|
|
@@ -1426,7 +1437,7 @@ function entityToTableDef(entityName, entity, configDir) {
|
|
|
1426
1437
|
}
|
|
1427
1438
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
1428
1439
|
const render = parseEntityRender(entity.render);
|
|
1429
|
-
const outputFile =
|
|
1440
|
+
const outputFile = entity.outputFile;
|
|
1430
1441
|
return {
|
|
1431
1442
|
columns,
|
|
1432
1443
|
render,
|
|
@@ -1808,19 +1819,32 @@ var Lattice = class {
|
|
|
1808
1819
|
const entries = Object.entries(withConventions).filter(([k]) => k !== "id");
|
|
1809
1820
|
if (entries.length === 0) return Promise.resolve(existing.id);
|
|
1810
1821
|
const setCols = entries.map(([k]) => `"${k}" = ?`).join(", ");
|
|
1811
|
-
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1812
|
-
|
|
1822
|
+
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1823
|
+
...entries.map(([, v]) => v),
|
|
1824
|
+
existing.id
|
|
1825
|
+
]);
|
|
1826
|
+
this._fireWriteHooks(
|
|
1827
|
+
table,
|
|
1828
|
+
"update",
|
|
1829
|
+
withConventions,
|
|
1830
|
+
existing.id,
|
|
1831
|
+
Object.keys(sanitized)
|
|
1832
|
+
);
|
|
1813
1833
|
return Promise.resolve(existing.id);
|
|
1814
1834
|
}
|
|
1815
1835
|
const id = sanitized.id ?? (0, import_uuid.v4)();
|
|
1816
1836
|
const insertData = { ...withConventions, id, [naturalKeyCol]: naturalKeyVal };
|
|
1817
1837
|
if (opts?.orgId && cols.has("org_id") && !insertData.org_id) insertData.org_id = opts.orgId;
|
|
1818
1838
|
if (cols.has("deleted_at")) insertData.deleted_at = null;
|
|
1819
|
-
if (cols.has("created_at") && !insertData.created_at)
|
|
1839
|
+
if (cols.has("created_at") && !insertData.created_at)
|
|
1840
|
+
insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1820
1841
|
const filtered = this._filterToSchemaColumns(table, insertData);
|
|
1821
1842
|
const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
|
|
1822
1843
|
const placeholders = Object.keys(filtered).map(() => "?").join(", ");
|
|
1823
|
-
this._adapter.run(
|
|
1844
|
+
this._adapter.run(
|
|
1845
|
+
`INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`,
|
|
1846
|
+
Object.values(filtered)
|
|
1847
|
+
);
|
|
1824
1848
|
this._fireWriteHooks(table, "insert", filtered, id);
|
|
1825
1849
|
return Promise.resolve(id);
|
|
1826
1850
|
}
|
|
@@ -1837,14 +1861,25 @@ var Lattice = class {
|
|
|
1837
1861
|
);
|
|
1838
1862
|
if (!existing) return Promise.resolve(false);
|
|
1839
1863
|
const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(data));
|
|
1840
|
-
const entries = Object.entries(sanitized).filter(
|
|
1864
|
+
const entries = Object.entries(sanitized).filter(
|
|
1865
|
+
([k, v]) => v !== null && v !== void 0 && k !== "id"
|
|
1866
|
+
);
|
|
1841
1867
|
if (entries.length === 0) return Promise.resolve(true);
|
|
1842
1868
|
const cols = this._ensureColumnCache(table);
|
|
1843
1869
|
const withTs = [...entries];
|
|
1844
1870
|
if (cols.has("updated_at")) withTs.push(["updated_at", (/* @__PURE__ */ new Date()).toISOString()]);
|
|
1845
1871
|
const setCols = withTs.map(([k]) => `"${k}" = ?`).join(", ");
|
|
1846
|
-
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1847
|
-
|
|
1872
|
+
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1873
|
+
...withTs.map(([, v]) => v),
|
|
1874
|
+
existing.id
|
|
1875
|
+
]);
|
|
1876
|
+
this._fireWriteHooks(
|
|
1877
|
+
table,
|
|
1878
|
+
"update",
|
|
1879
|
+
Object.fromEntries(entries),
|
|
1880
|
+
existing.id,
|
|
1881
|
+
entries.map(([k]) => k)
|
|
1882
|
+
);
|
|
1848
1883
|
return Promise.resolve(true);
|
|
1849
1884
|
}
|
|
1850
1885
|
/**
|
|
@@ -1922,7 +1957,10 @@ var Lattice = class {
|
|
|
1922
1957
|
const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
|
|
1923
1958
|
const placeholders = Object.keys(filtered).map(() => "?").join(", ");
|
|
1924
1959
|
const verb = opts?.upsert ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
|
|
1925
|
-
this._adapter.run(
|
|
1960
|
+
this._adapter.run(
|
|
1961
|
+
`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
|
|
1962
|
+
Object.values(filtered)
|
|
1963
|
+
);
|
|
1926
1964
|
return Promise.resolve();
|
|
1927
1965
|
}
|
|
1928
1966
|
/**
|
|
@@ -1934,7 +1972,10 @@ var Lattice = class {
|
|
|
1934
1972
|
const entries = Object.entries(conditions);
|
|
1935
1973
|
if (entries.length === 0) return Promise.resolve();
|
|
1936
1974
|
const where = entries.map(([k]) => `"${k}" = ?`).join(" AND ");
|
|
1937
|
-
this._adapter.run(
|
|
1975
|
+
this._adapter.run(
|
|
1976
|
+
`DELETE FROM "${junctionTable}" WHERE ${where}`,
|
|
1977
|
+
entries.map(([, v]) => v)
|
|
1978
|
+
);
|
|
1938
1979
|
return Promise.resolve();
|
|
1939
1980
|
}
|
|
1940
1981
|
// -------------------------------------------------------------------------
|
|
@@ -1961,7 +2002,13 @@ var Lattice = class {
|
|
|
1961
2002
|
if (config.sourceFile) upsertOpts.sourceFile = config.sourceFile;
|
|
1962
2003
|
if (config.sourceHash) upsertOpts.sourceHash = config.sourceHash;
|
|
1963
2004
|
if (config.orgId) upsertOpts.orgId = config.orgId;
|
|
1964
|
-
await this.upsertByNaturalKey(
|
|
2005
|
+
await this.upsertByNaturalKey(
|
|
2006
|
+
config.table,
|
|
2007
|
+
config.naturalKey,
|
|
2008
|
+
naturalKeyVal,
|
|
2009
|
+
record,
|
|
2010
|
+
upsertOpts
|
|
2011
|
+
);
|
|
1965
2012
|
upserted++;
|
|
1966
2013
|
if (config.linkTo) {
|
|
1967
2014
|
const recordId = await this.getByNaturalKey(config.table, config.naturalKey, naturalKeyVal);
|
|
@@ -1986,7 +2033,12 @@ var Lattice = class {
|
|
|
1986
2033
|
}
|
|
1987
2034
|
}
|
|
1988
2035
|
if (config.softDeleteMissing && config.sourceFile && keys.length > 0) {
|
|
1989
|
-
softDeleted = await this.softDeleteMissing(
|
|
2036
|
+
softDeleted = await this.softDeleteMissing(
|
|
2037
|
+
config.table,
|
|
2038
|
+
config.naturalKey,
|
|
2039
|
+
config.sourceFile,
|
|
2040
|
+
keys
|
|
2041
|
+
);
|
|
1990
2042
|
}
|
|
1991
2043
|
return { upserted, linked, softDeleted };
|
|
1992
2044
|
}
|
|
@@ -2070,7 +2122,10 @@ var Lattice = class {
|
|
|
2070
2122
|
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
2071
2123
|
const orderBy = section.query.orderBy ? ` ORDER BY "${section.query.orderBy}" ${section.query.orderDir === "desc" ? "DESC" : "ASC"}` : "";
|
|
2072
2124
|
const limit = section.query.limit ? ` LIMIT ${String(section.query.limit)}` : "";
|
|
2073
|
-
const rows = this._adapter.all(
|
|
2125
|
+
const rows = this._adapter.all(
|
|
2126
|
+
`SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`,
|
|
2127
|
+
params
|
|
2128
|
+
);
|
|
2074
2129
|
if (rows.length > 0) allEmpty = false;
|
|
2075
2130
|
let formatted = "";
|
|
2076
2131
|
if (section.format === "custom" && section.customFormat) {
|
|
@@ -2393,9 +2448,7 @@ var Lattice = class {
|
|
|
2393
2448
|
if (!known) return null;
|
|
2394
2449
|
for (const col of cols) {
|
|
2395
2450
|
if (!known.has(col)) {
|
|
2396
|
-
return Promise.reject(
|
|
2397
|
-
new Error(`Lattice: unknown column "${col}" in table "${table}"`)
|
|
2398
|
-
);
|
|
2451
|
+
return Promise.reject(new Error(`Lattice: unknown column "${col}" in table "${table}"`));
|
|
2399
2452
|
}
|
|
2400
2453
|
}
|
|
2401
2454
|
return null;
|
|
@@ -2479,7 +2532,9 @@ function parseBlock(block) {
|
|
|
2479
2532
|
return { error: { line, message: "Missing required field: op" } };
|
|
2480
2533
|
}
|
|
2481
2534
|
if (rawOp !== "create" && rawOp !== "update" && rawOp !== "delete") {
|
|
2482
|
-
return {
|
|
2535
|
+
return {
|
|
2536
|
+
error: { line, message: `Invalid op: "${rawOp}". Must be create, update, or delete` }
|
|
2537
|
+
};
|
|
2483
2538
|
}
|
|
2484
2539
|
const op = rawOp;
|
|
2485
2540
|
const table = header.table;
|
|
@@ -2487,7 +2542,9 @@ function parseBlock(block) {
|
|
|
2487
2542
|
return { error: { line, message: "Missing required field: table" } };
|
|
2488
2543
|
}
|
|
2489
2544
|
if (!TABLE_NAME_RE.test(table)) {
|
|
2490
|
-
return {
|
|
2545
|
+
return {
|
|
2546
|
+
error: { line, message: `Invalid table name: "${table}". Only [a-zA-Z0-9_] allowed` }
|
|
2547
|
+
};
|
|
2491
2548
|
}
|
|
2492
2549
|
const target = header.target || void 0;
|
|
2493
2550
|
if ((op === "update" || op === "delete") && !target) {
|
|
@@ -2629,7 +2686,12 @@ function parseSessionMD(content, startOffset = 0, options) {
|
|
|
2629
2686
|
writeFields[key] = (m[2] ?? "").trim();
|
|
2630
2687
|
}
|
|
2631
2688
|
}
|
|
2632
|
-
const newEntry = {
|
|
2689
|
+
const newEntry = {
|
|
2690
|
+
id: entryId,
|
|
2691
|
+
type: resolvedType,
|
|
2692
|
+
timestamp: headers.timestamp,
|
|
2693
|
+
body
|
|
2694
|
+
};
|
|
2633
2695
|
if (headers.project) newEntry.project = headers.project;
|
|
2634
2696
|
if (headers.task) newEntry.task = headers.task;
|
|
2635
2697
|
if (tags) newEntry.tags = tags;
|
|
@@ -2765,7 +2827,9 @@ function applyWriteEntry(db, entry) {
|
|
|
2765
2827
|
const allFields = { ...fields, id };
|
|
2766
2828
|
const cols = Object.keys(allFields).map((c) => `"${c}"`).join(", ");
|
|
2767
2829
|
const placeholders = Object.keys(allFields).map(() => "?").join(", ");
|
|
2768
|
-
db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(
|
|
2830
|
+
db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(
|
|
2831
|
+
...Object.values(allFields)
|
|
2832
|
+
);
|
|
2769
2833
|
recordId = id;
|
|
2770
2834
|
} else if (op === "update") {
|
|
2771
2835
|
if (!target) {
|
|
@@ -2773,7 +2837,10 @@ function applyWriteEntry(db, entry) {
|
|
|
2773
2837
|
}
|
|
2774
2838
|
const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
|
|
2775
2839
|
const setCols = Object.keys(fields).map((c) => `"${c}" = ?`).join(", ");
|
|
2776
|
-
db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(
|
|
2840
|
+
db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(
|
|
2841
|
+
...Object.values(fields),
|
|
2842
|
+
target
|
|
2843
|
+
);
|
|
2777
2844
|
recordId = target;
|
|
2778
2845
|
} else {
|
|
2779
2846
|
if (!target) {
|
|
@@ -2781,7 +2848,9 @@ function applyWriteEntry(db, entry) {
|
|
|
2781
2848
|
}
|
|
2782
2849
|
const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
|
|
2783
2850
|
if (knownColumns.has("deleted_at")) {
|
|
2784
|
-
db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(
|
|
2851
|
+
db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(
|
|
2852
|
+
target
|
|
2853
|
+
);
|
|
2785
2854
|
} else {
|
|
2786
2855
|
db.prepare(`DELETE FROM "${table}" WHERE "${pkCol}" = ?`).run(target);
|
|
2787
2856
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1403,8 +1403,9 @@ interface ParsedConfig {
|
|
|
1403
1403
|
/**
|
|
1404
1404
|
* Read, parse, and validate a `lattice.config.yml` file.
|
|
1405
1405
|
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1406
|
+
* The `db` path is resolved relative to the config file's directory.
|
|
1407
|
+
* `outputFile` values are kept as-is (they are relative to the `outputDir`
|
|
1408
|
+
* passed to `render()`, not to the config file location).
|
|
1408
1409
|
*
|
|
1409
1410
|
* @throws If the file cannot be read, the YAML is malformed, or required
|
|
1410
1411
|
* keys are missing.
|
|
@@ -1413,7 +1414,8 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
|
|
|
1413
1414
|
/**
|
|
1414
1415
|
* Parse and validate a raw YAML string as a Lattice config.
|
|
1415
1416
|
*
|
|
1416
|
-
* `configDir` is used to resolve
|
|
1417
|
+
* `configDir` is used to resolve the `db` path. `outputFile` values are kept
|
|
1418
|
+
* as-is (relative to the `outputDir` passed to `render()`).
|
|
1417
1419
|
* Typically this should be the directory that contains `lattice.config.yml`.
|
|
1418
1420
|
*
|
|
1419
1421
|
* Useful for testing without touching the filesystem.
|
|
@@ -1588,7 +1590,7 @@ declare const DEFAULT_TYPE_ALIASES: Readonly<Record<string, string>>;
|
|
|
1588
1590
|
* Each entry looks like:
|
|
1589
1591
|
* ```
|
|
1590
1592
|
* ---
|
|
1591
|
-
* id: 2026-03-12T15:30:42Z-
|
|
1593
|
+
* id: 2026-03-12T15:30:42Z-agent1-a1b2c3
|
|
1592
1594
|
* type: event
|
|
1593
1595
|
* timestamp: 2026-03-12T15:30:42Z
|
|
1594
1596
|
* ---
|
package/dist/index.d.ts
CHANGED
|
@@ -1403,8 +1403,9 @@ interface ParsedConfig {
|
|
|
1403
1403
|
/**
|
|
1404
1404
|
* Read, parse, and validate a `lattice.config.yml` file.
|
|
1405
1405
|
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1406
|
+
* The `db` path is resolved relative to the config file's directory.
|
|
1407
|
+
* `outputFile` values are kept as-is (they are relative to the `outputDir`
|
|
1408
|
+
* passed to `render()`, not to the config file location).
|
|
1408
1409
|
*
|
|
1409
1410
|
* @throws If the file cannot be read, the YAML is malformed, or required
|
|
1410
1411
|
* keys are missing.
|
|
@@ -1413,7 +1414,8 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
|
|
|
1413
1414
|
/**
|
|
1414
1415
|
* Parse and validate a raw YAML string as a Lattice config.
|
|
1415
1416
|
*
|
|
1416
|
-
* `configDir` is used to resolve
|
|
1417
|
+
* `configDir` is used to resolve the `db` path. `outputFile` values are kept
|
|
1418
|
+
* as-is (relative to the `outputDir` passed to `render()`).
|
|
1417
1419
|
* Typically this should be the directory that contains `lattice.config.yml`.
|
|
1418
1420
|
*
|
|
1419
1421
|
* Useful for testing without touching the filesystem.
|
|
@@ -1588,7 +1590,7 @@ declare const DEFAULT_TYPE_ALIASES: Readonly<Record<string, string>>;
|
|
|
1588
1590
|
* Each entry looks like:
|
|
1589
1591
|
* ```
|
|
1590
1592
|
* ---
|
|
1591
|
-
* id: 2026-03-12T15:30:42Z-
|
|
1593
|
+
* id: 2026-03-12T15:30:42Z-agent1-a1b2c3
|
|
1592
1594
|
* type: event
|
|
1593
1595
|
* timestamp: 2026-03-12T15:30:42Z
|
|
1594
1596
|
* ---
|
package/dist/index.js
CHANGED
|
@@ -214,7 +214,9 @@ var SchemaManager = class {
|
|
|
214
214
|
if (this._entityContexts.has(name)) {
|
|
215
215
|
const cols = adapter.all(`PRAGMA table_info("${name}")`);
|
|
216
216
|
const hasDeletedAt = cols.some((c) => c.name === "deleted_at");
|
|
217
|
-
return adapter.all(
|
|
217
|
+
return adapter.all(
|
|
218
|
+
`SELECT * FROM "${name}"${hasDeletedAt ? " WHERE deleted_at IS NULL" : ""}`
|
|
219
|
+
);
|
|
218
220
|
}
|
|
219
221
|
throw new Error(`Unknown table: "${name}"`);
|
|
220
222
|
}
|
|
@@ -793,7 +795,14 @@ var RenderEngine = class {
|
|
|
793
795
|
const slugs = new Set(rows.map((row) => def.slug(row)));
|
|
794
796
|
currentSlugsByTable.set(table, slugs);
|
|
795
797
|
}
|
|
796
|
-
return cleanupEntityContexts(
|
|
798
|
+
return cleanupEntityContexts(
|
|
799
|
+
outputDir,
|
|
800
|
+
entityContexts,
|
|
801
|
+
currentSlugsByTable,
|
|
802
|
+
prevManifest,
|
|
803
|
+
options,
|
|
804
|
+
newManifest
|
|
805
|
+
);
|
|
797
806
|
}
|
|
798
807
|
/**
|
|
799
808
|
* Render all entity context definitions.
|
|
@@ -908,7 +917,6 @@ var ReverseSyncEngine = class {
|
|
|
908
917
|
}
|
|
909
918
|
}
|
|
910
919
|
if (reverseSyncFiles.size === 0) continue;
|
|
911
|
-
const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
912
920
|
const allRows = this._schema.queryTable(this._adapter, table);
|
|
913
921
|
const slugToRow = /* @__PURE__ */ new Map();
|
|
914
922
|
for (const row of allRows) {
|
|
@@ -940,7 +948,7 @@ var ReverseSyncEngine = class {
|
|
|
940
948
|
const updates = reverseSyncFn(currentContent, entityRow);
|
|
941
949
|
if (updates.length === 0) continue;
|
|
942
950
|
if (!dryRun) {
|
|
943
|
-
this._applyUpdates(updates
|
|
951
|
+
this._applyUpdates(updates);
|
|
944
952
|
}
|
|
945
953
|
result.updatesApplied += updates.length;
|
|
946
954
|
} catch (err) {
|
|
@@ -959,7 +967,7 @@ var ReverseSyncEngine = class {
|
|
|
959
967
|
* Each update is an independent UPDATE statement.
|
|
960
968
|
* Wrapped in a transaction for atomicity.
|
|
961
969
|
*/
|
|
962
|
-
_applyUpdates(updates
|
|
970
|
+
_applyUpdates(updates) {
|
|
963
971
|
this._adapter.run("BEGIN");
|
|
964
972
|
try {
|
|
965
973
|
for (const update of updates) {
|
|
@@ -976,10 +984,7 @@ var ReverseSyncEngine = class {
|
|
|
976
984
|
const setClause = setCols.map((c) => `"${c}" = ?`).join(", ");
|
|
977
985
|
const whereClause = pkCols.map((c) => `"${c}" = ?`).join(" AND ");
|
|
978
986
|
const sql = `UPDATE "${update.table}" SET ${setClause} WHERE ${whereClause}`;
|
|
979
|
-
const params = [
|
|
980
|
-
...setCols.map((c) => update.set[c]),
|
|
981
|
-
...pkCols.map((c) => update.pk[c])
|
|
982
|
-
];
|
|
987
|
+
const params = [...setCols.map((c) => update.set[c]), ...pkCols.map((c) => update.pk[c])];
|
|
983
988
|
this._adapter.run(sql, params);
|
|
984
989
|
}
|
|
985
990
|
this._adapter.run("COMMIT");
|
|
@@ -1049,8 +1054,12 @@ var InMemoryStateStore = class {
|
|
|
1049
1054
|
return this._seen.get(filePath)?.has(key) ?? false;
|
|
1050
1055
|
}
|
|
1051
1056
|
markSeen(filePath, key) {
|
|
1052
|
-
|
|
1053
|
-
|
|
1057
|
+
let seenSet = this._seen.get(filePath);
|
|
1058
|
+
if (!seenSet) {
|
|
1059
|
+
seenSet = /* @__PURE__ */ new Set();
|
|
1060
|
+
this._seen.set(filePath, seenSet);
|
|
1061
|
+
}
|
|
1062
|
+
seenSet.add(key);
|
|
1054
1063
|
}
|
|
1055
1064
|
};
|
|
1056
1065
|
var SQLiteStateStore = class {
|
|
@@ -1089,11 +1098,13 @@ var SQLiteStateStore = class {
|
|
|
1089
1098
|
}
|
|
1090
1099
|
setOffset(filePath, offset, size) {
|
|
1091
1100
|
this._init();
|
|
1092
|
-
this._db.prepare(
|
|
1101
|
+
this._db.prepare(
|
|
1102
|
+
`
|
|
1093
1103
|
INSERT INTO _lattice_writeback_offset (file_path, byte_offset, file_size, updated_at)
|
|
1094
1104
|
VALUES (?, ?, ?, datetime('now'))
|
|
1095
1105
|
ON CONFLICT(file_path) DO UPDATE SET byte_offset = ?, file_size = ?, updated_at = datetime('now')
|
|
1096
|
-
`
|
|
1106
|
+
`
|
|
1107
|
+
).run(filePath, offset, size, offset, size);
|
|
1097
1108
|
}
|
|
1098
1109
|
isSeen(filePath, key) {
|
|
1099
1110
|
this._init();
|
|
@@ -1334,13 +1345,13 @@ function buildParsedConfig(raw, sourceName, configDir) {
|
|
|
1334
1345
|
const dbPath = resolve(configDir, config.db);
|
|
1335
1346
|
const tables = [];
|
|
1336
1347
|
for (const [entityName, entityDef] of Object.entries(config.entities)) {
|
|
1337
|
-
const definition = entityToTableDef(entityName, entityDef
|
|
1348
|
+
const definition = entityToTableDef(entityName, entityDef);
|
|
1338
1349
|
tables.push({ name: entityName, definition });
|
|
1339
1350
|
}
|
|
1340
1351
|
const entityContexts = parseEntityContexts(config.entityContexts);
|
|
1341
1352
|
return { dbPath, tables, entityContexts };
|
|
1342
1353
|
}
|
|
1343
|
-
function entityToTableDef(entityName, entity
|
|
1354
|
+
function entityToTableDef(entityName, entity) {
|
|
1344
1355
|
const rawFields = entity.fields;
|
|
1345
1356
|
if (!rawFields || typeof rawFields !== "object" || Array.isArray(rawFields)) {
|
|
1346
1357
|
throw new Error(`Lattice: entity "${entityName}" must have a "fields" object`);
|
|
@@ -1364,7 +1375,7 @@ function entityToTableDef(entityName, entity, configDir) {
|
|
|
1364
1375
|
}
|
|
1365
1376
|
const primaryKey = entity.primaryKey ?? pkFromField;
|
|
1366
1377
|
const render = parseEntityRender(entity.render);
|
|
1367
|
-
const outputFile =
|
|
1378
|
+
const outputFile = entity.outputFile;
|
|
1368
1379
|
return {
|
|
1369
1380
|
columns,
|
|
1370
1381
|
render,
|
|
@@ -1746,19 +1757,32 @@ var Lattice = class {
|
|
|
1746
1757
|
const entries = Object.entries(withConventions).filter(([k]) => k !== "id");
|
|
1747
1758
|
if (entries.length === 0) return Promise.resolve(existing.id);
|
|
1748
1759
|
const setCols = entries.map(([k]) => `"${k}" = ?`).join(", ");
|
|
1749
|
-
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1750
|
-
|
|
1760
|
+
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1761
|
+
...entries.map(([, v]) => v),
|
|
1762
|
+
existing.id
|
|
1763
|
+
]);
|
|
1764
|
+
this._fireWriteHooks(
|
|
1765
|
+
table,
|
|
1766
|
+
"update",
|
|
1767
|
+
withConventions,
|
|
1768
|
+
existing.id,
|
|
1769
|
+
Object.keys(sanitized)
|
|
1770
|
+
);
|
|
1751
1771
|
return Promise.resolve(existing.id);
|
|
1752
1772
|
}
|
|
1753
1773
|
const id = sanitized.id ?? uuidv4();
|
|
1754
1774
|
const insertData = { ...withConventions, id, [naturalKeyCol]: naturalKeyVal };
|
|
1755
1775
|
if (opts?.orgId && cols.has("org_id") && !insertData.org_id) insertData.org_id = opts.orgId;
|
|
1756
1776
|
if (cols.has("deleted_at")) insertData.deleted_at = null;
|
|
1757
|
-
if (cols.has("created_at") && !insertData.created_at)
|
|
1777
|
+
if (cols.has("created_at") && !insertData.created_at)
|
|
1778
|
+
insertData.created_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1758
1779
|
const filtered = this._filterToSchemaColumns(table, insertData);
|
|
1759
1780
|
const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
|
|
1760
1781
|
const placeholders = Object.keys(filtered).map(() => "?").join(", ");
|
|
1761
|
-
this._adapter.run(
|
|
1782
|
+
this._adapter.run(
|
|
1783
|
+
`INSERT INTO "${table}" (${colNames}) VALUES (${placeholders})`,
|
|
1784
|
+
Object.values(filtered)
|
|
1785
|
+
);
|
|
1762
1786
|
this._fireWriteHooks(table, "insert", filtered, id);
|
|
1763
1787
|
return Promise.resolve(id);
|
|
1764
1788
|
}
|
|
@@ -1775,14 +1799,25 @@ var Lattice = class {
|
|
|
1775
1799
|
);
|
|
1776
1800
|
if (!existing) return Promise.resolve(false);
|
|
1777
1801
|
const sanitized = this._filterToSchemaColumns(table, this._sanitizer.sanitizeRow(data));
|
|
1778
|
-
const entries = Object.entries(sanitized).filter(
|
|
1802
|
+
const entries = Object.entries(sanitized).filter(
|
|
1803
|
+
([k, v]) => v !== null && v !== void 0 && k !== "id"
|
|
1804
|
+
);
|
|
1779
1805
|
if (entries.length === 0) return Promise.resolve(true);
|
|
1780
1806
|
const cols = this._ensureColumnCache(table);
|
|
1781
1807
|
const withTs = [...entries];
|
|
1782
1808
|
if (cols.has("updated_at")) withTs.push(["updated_at", (/* @__PURE__ */ new Date()).toISOString()]);
|
|
1783
1809
|
const setCols = withTs.map(([k]) => `"${k}" = ?`).join(", ");
|
|
1784
|
-
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1785
|
-
|
|
1810
|
+
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE id = ?`, [
|
|
1811
|
+
...withTs.map(([, v]) => v),
|
|
1812
|
+
existing.id
|
|
1813
|
+
]);
|
|
1814
|
+
this._fireWriteHooks(
|
|
1815
|
+
table,
|
|
1816
|
+
"update",
|
|
1817
|
+
Object.fromEntries(entries),
|
|
1818
|
+
existing.id,
|
|
1819
|
+
entries.map(([k]) => k)
|
|
1820
|
+
);
|
|
1786
1821
|
return Promise.resolve(true);
|
|
1787
1822
|
}
|
|
1788
1823
|
/**
|
|
@@ -1860,7 +1895,10 @@ var Lattice = class {
|
|
|
1860
1895
|
const colNames = Object.keys(filtered).map((c) => `"${c}"`).join(", ");
|
|
1861
1896
|
const placeholders = Object.keys(filtered).map(() => "?").join(", ");
|
|
1862
1897
|
const verb = opts?.upsert ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
|
|
1863
|
-
this._adapter.run(
|
|
1898
|
+
this._adapter.run(
|
|
1899
|
+
`${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
|
|
1900
|
+
Object.values(filtered)
|
|
1901
|
+
);
|
|
1864
1902
|
return Promise.resolve();
|
|
1865
1903
|
}
|
|
1866
1904
|
/**
|
|
@@ -1872,7 +1910,10 @@ var Lattice = class {
|
|
|
1872
1910
|
const entries = Object.entries(conditions);
|
|
1873
1911
|
if (entries.length === 0) return Promise.resolve();
|
|
1874
1912
|
const where = entries.map(([k]) => `"${k}" = ?`).join(" AND ");
|
|
1875
|
-
this._adapter.run(
|
|
1913
|
+
this._adapter.run(
|
|
1914
|
+
`DELETE FROM "${junctionTable}" WHERE ${where}`,
|
|
1915
|
+
entries.map(([, v]) => v)
|
|
1916
|
+
);
|
|
1876
1917
|
return Promise.resolve();
|
|
1877
1918
|
}
|
|
1878
1919
|
// -------------------------------------------------------------------------
|
|
@@ -1899,7 +1940,13 @@ var Lattice = class {
|
|
|
1899
1940
|
if (config.sourceFile) upsertOpts.sourceFile = config.sourceFile;
|
|
1900
1941
|
if (config.sourceHash) upsertOpts.sourceHash = config.sourceHash;
|
|
1901
1942
|
if (config.orgId) upsertOpts.orgId = config.orgId;
|
|
1902
|
-
await this.upsertByNaturalKey(
|
|
1943
|
+
await this.upsertByNaturalKey(
|
|
1944
|
+
config.table,
|
|
1945
|
+
config.naturalKey,
|
|
1946
|
+
naturalKeyVal,
|
|
1947
|
+
record,
|
|
1948
|
+
upsertOpts
|
|
1949
|
+
);
|
|
1903
1950
|
upserted++;
|
|
1904
1951
|
if (config.linkTo) {
|
|
1905
1952
|
const recordId = await this.getByNaturalKey(config.table, config.naturalKey, naturalKeyVal);
|
|
@@ -1924,7 +1971,12 @@ var Lattice = class {
|
|
|
1924
1971
|
}
|
|
1925
1972
|
}
|
|
1926
1973
|
if (config.softDeleteMissing && config.sourceFile && keys.length > 0) {
|
|
1927
|
-
softDeleted = await this.softDeleteMissing(
|
|
1974
|
+
softDeleted = await this.softDeleteMissing(
|
|
1975
|
+
config.table,
|
|
1976
|
+
config.naturalKey,
|
|
1977
|
+
config.sourceFile,
|
|
1978
|
+
keys
|
|
1979
|
+
);
|
|
1928
1980
|
}
|
|
1929
1981
|
return { upserted, linked, softDeleted };
|
|
1930
1982
|
}
|
|
@@ -2008,7 +2060,10 @@ var Lattice = class {
|
|
|
2008
2060
|
const where = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
|
|
2009
2061
|
const orderBy = section.query.orderBy ? ` ORDER BY "${section.query.orderBy}" ${section.query.orderDir === "desc" ? "DESC" : "ASC"}` : "";
|
|
2010
2062
|
const limit = section.query.limit ? ` LIMIT ${String(section.query.limit)}` : "";
|
|
2011
|
-
const rows = this._adapter.all(
|
|
2063
|
+
const rows = this._adapter.all(
|
|
2064
|
+
`SELECT * FROM "${section.query.table}"${where}${orderBy}${limit}`,
|
|
2065
|
+
params
|
|
2066
|
+
);
|
|
2012
2067
|
if (rows.length > 0) allEmpty = false;
|
|
2013
2068
|
let formatted = "";
|
|
2014
2069
|
if (section.format === "custom" && section.customFormat) {
|
|
@@ -2331,9 +2386,7 @@ var Lattice = class {
|
|
|
2331
2386
|
if (!known) return null;
|
|
2332
2387
|
for (const col of cols) {
|
|
2333
2388
|
if (!known.has(col)) {
|
|
2334
|
-
return Promise.reject(
|
|
2335
|
-
new Error(`Lattice: unknown column "${col}" in table "${table}"`)
|
|
2336
|
-
);
|
|
2389
|
+
return Promise.reject(new Error(`Lattice: unknown column "${col}" in table "${table}"`));
|
|
2337
2390
|
}
|
|
2338
2391
|
}
|
|
2339
2392
|
return null;
|
|
@@ -2417,7 +2470,9 @@ function parseBlock(block) {
|
|
|
2417
2470
|
return { error: { line, message: "Missing required field: op" } };
|
|
2418
2471
|
}
|
|
2419
2472
|
if (rawOp !== "create" && rawOp !== "update" && rawOp !== "delete") {
|
|
2420
|
-
return {
|
|
2473
|
+
return {
|
|
2474
|
+
error: { line, message: `Invalid op: "${rawOp}". Must be create, update, or delete` }
|
|
2475
|
+
};
|
|
2421
2476
|
}
|
|
2422
2477
|
const op = rawOp;
|
|
2423
2478
|
const table = header.table;
|
|
@@ -2425,7 +2480,9 @@ function parseBlock(block) {
|
|
|
2425
2480
|
return { error: { line, message: "Missing required field: table" } };
|
|
2426
2481
|
}
|
|
2427
2482
|
if (!TABLE_NAME_RE.test(table)) {
|
|
2428
|
-
return {
|
|
2483
|
+
return {
|
|
2484
|
+
error: { line, message: `Invalid table name: "${table}". Only [a-zA-Z0-9_] allowed` }
|
|
2485
|
+
};
|
|
2429
2486
|
}
|
|
2430
2487
|
const target = header.target || void 0;
|
|
2431
2488
|
if ((op === "update" || op === "delete") && !target) {
|
|
@@ -2567,7 +2624,12 @@ function parseSessionMD(content, startOffset = 0, options) {
|
|
|
2567
2624
|
writeFields[key] = (m[2] ?? "").trim();
|
|
2568
2625
|
}
|
|
2569
2626
|
}
|
|
2570
|
-
const newEntry = {
|
|
2627
|
+
const newEntry = {
|
|
2628
|
+
id: entryId,
|
|
2629
|
+
type: resolvedType,
|
|
2630
|
+
timestamp: headers.timestamp,
|
|
2631
|
+
body
|
|
2632
|
+
};
|
|
2571
2633
|
if (headers.project) newEntry.project = headers.project;
|
|
2572
2634
|
if (headers.task) newEntry.task = headers.task;
|
|
2573
2635
|
if (tags) newEntry.tags = tags;
|
|
@@ -2703,7 +2765,9 @@ function applyWriteEntry(db, entry) {
|
|
|
2703
2765
|
const allFields = { ...fields, id };
|
|
2704
2766
|
const cols = Object.keys(allFields).map((c) => `"${c}"`).join(", ");
|
|
2705
2767
|
const placeholders = Object.keys(allFields).map(() => "?").join(", ");
|
|
2706
|
-
db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(
|
|
2768
|
+
db.prepare(`INSERT INTO "${table}" (${cols}) VALUES (${placeholders})`).run(
|
|
2769
|
+
...Object.values(allFields)
|
|
2770
|
+
);
|
|
2707
2771
|
recordId = id;
|
|
2708
2772
|
} else if (op === "update") {
|
|
2709
2773
|
if (!target) {
|
|
@@ -2711,7 +2775,10 @@ function applyWriteEntry(db, entry) {
|
|
|
2711
2775
|
}
|
|
2712
2776
|
const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
|
|
2713
2777
|
const setCols = Object.keys(fields).map((c) => `"${c}" = ?`).join(", ");
|
|
2714
|
-
db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(
|
|
2778
|
+
db.prepare(`UPDATE "${table}" SET ${setCols} WHERE "${pkCol}" = ?`).run(
|
|
2779
|
+
...Object.values(fields),
|
|
2780
|
+
target
|
|
2781
|
+
);
|
|
2715
2782
|
recordId = target;
|
|
2716
2783
|
} else {
|
|
2717
2784
|
if (!target) {
|
|
@@ -2719,7 +2786,9 @@ function applyWriteEntry(db, entry) {
|
|
|
2719
2786
|
}
|
|
2720
2787
|
const pkCol = columnRows.find((r) => r.name === "id") ? "id" : columnRows[0]?.name ?? "id";
|
|
2721
2788
|
if (knownColumns.has("deleted_at")) {
|
|
2722
|
-
db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(
|
|
2789
|
+
db.prepare(`UPDATE "${table}" SET deleted_at = datetime('now') WHERE "${pkCol}" = ?`).run(
|
|
2790
|
+
target
|
|
2791
|
+
);
|
|
2723
2792
|
} else {
|
|
2724
2793
|
db.prepare(`DELETE FROM "${table}" WHERE "${pkCol}" = ?`).run(target);
|
|
2725
2794
|
}
|