latticesql 0.8.0 → 0.10.0
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/dist/cli.js +189 -1
- package/dist/index.cjs +196 -47
- package/dist/index.d.cts +124 -3
- package/dist/index.d.ts +124 -3
- package/dist/index.js +196 -47
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -757,6 +757,165 @@ function truncateContent(content, budget) {
|
|
|
757
757
|
return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
|
|
758
758
|
}
|
|
759
759
|
|
|
760
|
+
// src/render/markdown.ts
|
|
761
|
+
function frontmatter(fields) {
|
|
762
|
+
const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
|
|
763
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
764
|
+
lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
|
|
765
|
+
}
|
|
766
|
+
return `---
|
|
767
|
+
${lines.join("\n")}
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
`;
|
|
771
|
+
}
|
|
772
|
+
function markdownTable(rows, columns) {
|
|
773
|
+
if (rows.length === 0 || columns.length === 0) return "";
|
|
774
|
+
const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
|
|
775
|
+
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
776
|
+
const body = rows.map((row) => {
|
|
777
|
+
const cells = columns.map((col) => {
|
|
778
|
+
const raw = row[col.key];
|
|
779
|
+
return col.format ? col.format(raw, row) : String(raw ?? "");
|
|
780
|
+
});
|
|
781
|
+
return "| " + cells.join(" | ") + " |";
|
|
782
|
+
});
|
|
783
|
+
return [header, separator, ...body].join("\n") + "\n";
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/session/constants.ts
|
|
787
|
+
function createReadOnlyHeader(options) {
|
|
788
|
+
const generator = options?.generator ?? "Lattice";
|
|
789
|
+
const docsRef = options?.docsRef ?? "the Lattice documentation";
|
|
790
|
+
return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
|
|
791
|
+
To update data in Lattice: write entries to SESSION.md in this directory.
|
|
792
|
+
Format: type: write | op: create/update/delete | table: <name> | target: <id>
|
|
793
|
+
See ${docsRef} for the SESSION.md format spec. -->
|
|
794
|
+
|
|
795
|
+
`;
|
|
796
|
+
}
|
|
797
|
+
var READ_ONLY_HEADER = createReadOnlyHeader();
|
|
798
|
+
|
|
799
|
+
// src/render/entity-templates.ts
|
|
800
|
+
var DEFAULT_HEADER = createReadOnlyHeader();
|
|
801
|
+
function compileEntityRender(spec) {
|
|
802
|
+
if (typeof spec === "function") return spec;
|
|
803
|
+
return compileTemplate(spec);
|
|
804
|
+
}
|
|
805
|
+
function compileTemplate(tmpl) {
|
|
806
|
+
switch (tmpl.template) {
|
|
807
|
+
case "entity-table":
|
|
808
|
+
return compileEntityTable(tmpl);
|
|
809
|
+
case "entity-profile":
|
|
810
|
+
return compileEntityProfile(tmpl);
|
|
811
|
+
case "entity-sections":
|
|
812
|
+
return compileEntitySections(tmpl);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function compileEntityTable(tmpl) {
|
|
816
|
+
return (rows) => {
|
|
817
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
818
|
+
let md = DEFAULT_HEADER;
|
|
819
|
+
md += frontmatter(tmpl.frontmatter ?? {});
|
|
820
|
+
md += `# ${tmpl.heading}
|
|
821
|
+
|
|
822
|
+
`;
|
|
823
|
+
if (data.length === 0) {
|
|
824
|
+
md += tmpl.emptyMessage ?? "*No data.*\n";
|
|
825
|
+
} else {
|
|
826
|
+
md += markdownTable(data, tmpl.columns);
|
|
827
|
+
}
|
|
828
|
+
return md;
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function compileEntityProfile(tmpl) {
|
|
832
|
+
return (rows) => {
|
|
833
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
834
|
+
const r = data[0];
|
|
835
|
+
if (!r) return "";
|
|
836
|
+
let md = DEFAULT_HEADER;
|
|
837
|
+
if (tmpl.frontmatter) {
|
|
838
|
+
const fm = typeof tmpl.frontmatter === "function" ? tmpl.frontmatter(r) : tmpl.frontmatter;
|
|
839
|
+
md += frontmatter(fm);
|
|
840
|
+
} else {
|
|
841
|
+
md += frontmatter({});
|
|
842
|
+
}
|
|
843
|
+
const heading = typeof tmpl.heading === "function" ? tmpl.heading(r) : tmpl.heading;
|
|
844
|
+
md += `# ${heading}
|
|
845
|
+
|
|
846
|
+
`;
|
|
847
|
+
for (const field of tmpl.fields) {
|
|
848
|
+
const val = r[field.key];
|
|
849
|
+
if (val === null || val === void 0) continue;
|
|
850
|
+
const formatted = field.format ? field.format(val, r) : String(val);
|
|
851
|
+
if (formatted) {
|
|
852
|
+
md += `**${field.label}:** ${formatted}
|
|
853
|
+
`;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
if (tmpl.sections) {
|
|
857
|
+
for (const section of tmpl.sections) {
|
|
858
|
+
const rawJson = r[`_${section.key}`];
|
|
859
|
+
if (!rawJson) continue;
|
|
860
|
+
if (section.condition && !section.condition(r)) continue;
|
|
861
|
+
const items = JSON.parse(rawJson);
|
|
862
|
+
if (items.length === 0) continue;
|
|
863
|
+
const sectionHeading = typeof section.heading === "function" ? section.heading(r) : section.heading;
|
|
864
|
+
md += `
|
|
865
|
+
## ${sectionHeading}
|
|
866
|
+
|
|
867
|
+
`;
|
|
868
|
+
if (section.render === "table" && section.columns) {
|
|
869
|
+
md += markdownTable(items, section.columns);
|
|
870
|
+
} else if (section.render === "list" && section.formatItem) {
|
|
871
|
+
for (const item of items) {
|
|
872
|
+
md += `- ${section.formatItem(item)}
|
|
873
|
+
`;
|
|
874
|
+
}
|
|
875
|
+
} else if (typeof section.render === "function") {
|
|
876
|
+
md += section.render(items);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return md;
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function compileEntitySections(tmpl) {
|
|
884
|
+
return (rows) => {
|
|
885
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
886
|
+
let md = DEFAULT_HEADER;
|
|
887
|
+
md += frontmatter(tmpl.frontmatter ?? {});
|
|
888
|
+
md += `# ${tmpl.heading}
|
|
889
|
+
|
|
890
|
+
`;
|
|
891
|
+
if (data.length === 0) {
|
|
892
|
+
md += tmpl.emptyMessage ?? "*No data.*\n";
|
|
893
|
+
return md;
|
|
894
|
+
}
|
|
895
|
+
for (const row of data) {
|
|
896
|
+
md += `## ${tmpl.perRow.heading(row)}
|
|
897
|
+
`;
|
|
898
|
+
if (tmpl.perRow.metadata?.length) {
|
|
899
|
+
const parts = tmpl.perRow.metadata.map((m) => {
|
|
900
|
+
const val = row[m.key];
|
|
901
|
+
const formatted = m.format ? m.format(val) : String(val ?? "");
|
|
902
|
+
return `**${m.label}:** ${formatted}`;
|
|
903
|
+
}).filter(Boolean);
|
|
904
|
+
if (parts.length > 0) {
|
|
905
|
+
md += parts.join(" | ") + "\n";
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (tmpl.perRow.body) {
|
|
909
|
+
md += `
|
|
910
|
+
${tmpl.perRow.body(row)}
|
|
911
|
+
`;
|
|
912
|
+
}
|
|
913
|
+
md += "\n";
|
|
914
|
+
}
|
|
915
|
+
return md;
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
760
919
|
// src/lifecycle/cleanup.ts
|
|
761
920
|
import { join as join4 } from "path";
|
|
762
921
|
import { existsSync as existsSync4, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
|
|
@@ -966,7 +1125,8 @@ var RenderEngine = class {
|
|
|
966
1125
|
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
967
1126
|
const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
|
|
968
1127
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
969
|
-
const
|
|
1128
|
+
const renderFn = compileEntityRender(spec.render);
|
|
1129
|
+
const content = truncateContent(renderFn(rows), spec.budget);
|
|
970
1130
|
renderedFiles.set(filename, content);
|
|
971
1131
|
const filePath = join5(entityDir, filename);
|
|
972
1132
|
if (atomicWrite(filePath, content)) {
|
|
@@ -1229,6 +1389,7 @@ var Lattice = class {
|
|
|
1229
1389
|
_renderHandlers = [];
|
|
1230
1390
|
_writebackHandlers = [];
|
|
1231
1391
|
_errorHandlers = [];
|
|
1392
|
+
_writeHooks = [];
|
|
1232
1393
|
constructor(pathOrConfig, options = {}) {
|
|
1233
1394
|
let dbPath;
|
|
1234
1395
|
let configTables;
|
|
@@ -1288,6 +1449,14 @@ var Lattice = class {
|
|
|
1288
1449
|
this._schema.defineEntityContext(table, def);
|
|
1289
1450
|
return this;
|
|
1290
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Register a write hook that fires after insert/update/delete operations.
|
|
1454
|
+
* Hooks run synchronously after the DB write and audit emit.
|
|
1455
|
+
*/
|
|
1456
|
+
defineWriteHook(hook) {
|
|
1457
|
+
this._writeHooks.push(hook);
|
|
1458
|
+
return this;
|
|
1459
|
+
}
|
|
1291
1460
|
defineWriteback(def) {
|
|
1292
1461
|
this._writeback.define(def);
|
|
1293
1462
|
return this;
|
|
@@ -1337,6 +1506,7 @@ var Lattice = class {
|
|
|
1337
1506
|
const rawPk = rowWithPk[pkCol];
|
|
1338
1507
|
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
1339
1508
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
1509
|
+
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
1340
1510
|
return Promise.resolve(pkValue);
|
|
1341
1511
|
}
|
|
1342
1512
|
upsert(table, row) {
|
|
@@ -1390,6 +1560,7 @@ var Lattice = class {
|
|
|
1390
1560
|
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE ${clause}`, values);
|
|
1391
1561
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1392
1562
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
1563
|
+
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
1393
1564
|
return Promise.resolve();
|
|
1394
1565
|
}
|
|
1395
1566
|
delete(table, id) {
|
|
@@ -1399,6 +1570,7 @@ var Lattice = class {
|
|
|
1399
1570
|
this._adapter.run(`DELETE FROM "${table}" WHERE ${clause}`, params);
|
|
1400
1571
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1401
1572
|
this._sanitizer.emitAudit(table, "delete", auditId);
|
|
1573
|
+
this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
|
|
1402
1574
|
return Promise.resolve();
|
|
1403
1575
|
}
|
|
1404
1576
|
get(table, id) {
|
|
@@ -1636,6 +1808,22 @@ var Lattice = class {
|
|
|
1636
1808
|
return { clauses, params };
|
|
1637
1809
|
}
|
|
1638
1810
|
/** Returns a rejected Promise if not initialized; null if ready. */
|
|
1811
|
+
_fireWriteHooks(table, op, row, pk, changedColumns) {
|
|
1812
|
+
for (const hook of this._writeHooks) {
|
|
1813
|
+
if (hook.table !== table) continue;
|
|
1814
|
+
if (!hook.on.includes(op)) continue;
|
|
1815
|
+
if (op === "update" && hook.watchColumns && changedColumns) {
|
|
1816
|
+
if (!hook.watchColumns.some((c) => changedColumns.includes(c))) continue;
|
|
1817
|
+
}
|
|
1818
|
+
try {
|
|
1819
|
+
const ctx = { table, op, row, pk };
|
|
1820
|
+
if (changedColumns) ctx.changedColumns = changedColumns;
|
|
1821
|
+
hook.handler(ctx);
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
for (const h of this._errorHandlers) h(err instanceof Error ? err : new Error(String(err)));
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1639
1827
|
_notInitError() {
|
|
1640
1828
|
if (!this._initialized) {
|
|
1641
1829
|
return Promise.reject(
|
package/dist/index.cjs
CHANGED
|
@@ -499,6 +499,172 @@ function truncateContent(content, budget) {
|
|
|
499
499
|
return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
// src/render/markdown.ts
|
|
503
|
+
function frontmatter(fields) {
|
|
504
|
+
const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
|
|
505
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
506
|
+
lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
|
|
507
|
+
}
|
|
508
|
+
return `---
|
|
509
|
+
${lines.join("\n")}
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
`;
|
|
513
|
+
}
|
|
514
|
+
function markdownTable(rows, columns) {
|
|
515
|
+
if (rows.length === 0 || columns.length === 0) return "";
|
|
516
|
+
const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
|
|
517
|
+
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
518
|
+
const body = rows.map((row) => {
|
|
519
|
+
const cells = columns.map((col) => {
|
|
520
|
+
const raw = row[col.key];
|
|
521
|
+
return col.format ? col.format(raw, row) : String(raw ?? "");
|
|
522
|
+
});
|
|
523
|
+
return "| " + cells.join(" | ") + " |";
|
|
524
|
+
});
|
|
525
|
+
return [header, separator, ...body].join("\n") + "\n";
|
|
526
|
+
}
|
|
527
|
+
function slugify(name) {
|
|
528
|
+
return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
529
|
+
}
|
|
530
|
+
function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
|
|
531
|
+
if (content.length <= maxChars) return content;
|
|
532
|
+
return content.slice(0, maxChars) + notice;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/session/constants.ts
|
|
536
|
+
function createReadOnlyHeader(options) {
|
|
537
|
+
const generator = options?.generator ?? "Lattice";
|
|
538
|
+
const docsRef = options?.docsRef ?? "the Lattice documentation";
|
|
539
|
+
return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
|
|
540
|
+
To update data in Lattice: write entries to SESSION.md in this directory.
|
|
541
|
+
Format: type: write | op: create/update/delete | table: <name> | target: <id>
|
|
542
|
+
See ${docsRef} for the SESSION.md format spec. -->
|
|
543
|
+
|
|
544
|
+
`;
|
|
545
|
+
}
|
|
546
|
+
var READ_ONLY_HEADER = createReadOnlyHeader();
|
|
547
|
+
|
|
548
|
+
// src/render/entity-templates.ts
|
|
549
|
+
var DEFAULT_HEADER = createReadOnlyHeader();
|
|
550
|
+
function compileEntityRender(spec) {
|
|
551
|
+
if (typeof spec === "function") return spec;
|
|
552
|
+
return compileTemplate(spec);
|
|
553
|
+
}
|
|
554
|
+
function compileTemplate(tmpl) {
|
|
555
|
+
switch (tmpl.template) {
|
|
556
|
+
case "entity-table":
|
|
557
|
+
return compileEntityTable(tmpl);
|
|
558
|
+
case "entity-profile":
|
|
559
|
+
return compileEntityProfile(tmpl);
|
|
560
|
+
case "entity-sections":
|
|
561
|
+
return compileEntitySections(tmpl);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function compileEntityTable(tmpl) {
|
|
565
|
+
return (rows) => {
|
|
566
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
567
|
+
let md = DEFAULT_HEADER;
|
|
568
|
+
md += frontmatter(tmpl.frontmatter ?? {});
|
|
569
|
+
md += `# ${tmpl.heading}
|
|
570
|
+
|
|
571
|
+
`;
|
|
572
|
+
if (data.length === 0) {
|
|
573
|
+
md += tmpl.emptyMessage ?? "*No data.*\n";
|
|
574
|
+
} else {
|
|
575
|
+
md += markdownTable(data, tmpl.columns);
|
|
576
|
+
}
|
|
577
|
+
return md;
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function compileEntityProfile(tmpl) {
|
|
581
|
+
return (rows) => {
|
|
582
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
583
|
+
const r = data[0];
|
|
584
|
+
if (!r) return "";
|
|
585
|
+
let md = DEFAULT_HEADER;
|
|
586
|
+
if (tmpl.frontmatter) {
|
|
587
|
+
const fm = typeof tmpl.frontmatter === "function" ? tmpl.frontmatter(r) : tmpl.frontmatter;
|
|
588
|
+
md += frontmatter(fm);
|
|
589
|
+
} else {
|
|
590
|
+
md += frontmatter({});
|
|
591
|
+
}
|
|
592
|
+
const heading = typeof tmpl.heading === "function" ? tmpl.heading(r) : tmpl.heading;
|
|
593
|
+
md += `# ${heading}
|
|
594
|
+
|
|
595
|
+
`;
|
|
596
|
+
for (const field of tmpl.fields) {
|
|
597
|
+
const val = r[field.key];
|
|
598
|
+
if (val === null || val === void 0) continue;
|
|
599
|
+
const formatted = field.format ? field.format(val, r) : String(val);
|
|
600
|
+
if (formatted) {
|
|
601
|
+
md += `**${field.label}:** ${formatted}
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (tmpl.sections) {
|
|
606
|
+
for (const section of tmpl.sections) {
|
|
607
|
+
const rawJson = r[`_${section.key}`];
|
|
608
|
+
if (!rawJson) continue;
|
|
609
|
+
if (section.condition && !section.condition(r)) continue;
|
|
610
|
+
const items = JSON.parse(rawJson);
|
|
611
|
+
if (items.length === 0) continue;
|
|
612
|
+
const sectionHeading = typeof section.heading === "function" ? section.heading(r) : section.heading;
|
|
613
|
+
md += `
|
|
614
|
+
## ${sectionHeading}
|
|
615
|
+
|
|
616
|
+
`;
|
|
617
|
+
if (section.render === "table" && section.columns) {
|
|
618
|
+
md += markdownTable(items, section.columns);
|
|
619
|
+
} else if (section.render === "list" && section.formatItem) {
|
|
620
|
+
for (const item of items) {
|
|
621
|
+
md += `- ${section.formatItem(item)}
|
|
622
|
+
`;
|
|
623
|
+
}
|
|
624
|
+
} else if (typeof section.render === "function") {
|
|
625
|
+
md += section.render(items);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return md;
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
function compileEntitySections(tmpl) {
|
|
633
|
+
return (rows) => {
|
|
634
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
635
|
+
let md = DEFAULT_HEADER;
|
|
636
|
+
md += frontmatter(tmpl.frontmatter ?? {});
|
|
637
|
+
md += `# ${tmpl.heading}
|
|
638
|
+
|
|
639
|
+
`;
|
|
640
|
+
if (data.length === 0) {
|
|
641
|
+
md += tmpl.emptyMessage ?? "*No data.*\n";
|
|
642
|
+
return md;
|
|
643
|
+
}
|
|
644
|
+
for (const row of data) {
|
|
645
|
+
md += `## ${tmpl.perRow.heading(row)}
|
|
646
|
+
`;
|
|
647
|
+
if (tmpl.perRow.metadata?.length) {
|
|
648
|
+
const parts = tmpl.perRow.metadata.map((m) => {
|
|
649
|
+
const val = row[m.key];
|
|
650
|
+
const formatted = m.format ? m.format(val) : String(val ?? "");
|
|
651
|
+
return `**${m.label}:** ${formatted}`;
|
|
652
|
+
}).filter(Boolean);
|
|
653
|
+
if (parts.length > 0) {
|
|
654
|
+
md += parts.join(" | ") + "\n";
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (tmpl.perRow.body) {
|
|
658
|
+
md += `
|
|
659
|
+
${tmpl.perRow.body(row)}
|
|
660
|
+
`;
|
|
661
|
+
}
|
|
662
|
+
md += "\n";
|
|
663
|
+
}
|
|
664
|
+
return md;
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
502
668
|
// src/lifecycle/cleanup.ts
|
|
503
669
|
var import_node_path3 = require("path");
|
|
504
670
|
var import_node_fs3 = require("fs");
|
|
@@ -708,7 +874,8 @@ var RenderEngine = class {
|
|
|
708
874
|
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
709
875
|
const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
|
|
710
876
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
711
|
-
const
|
|
877
|
+
const renderFn = compileEntityRender(spec.render);
|
|
878
|
+
const content = truncateContent(renderFn(rows), spec.budget);
|
|
712
879
|
renderedFiles.set(filename, content);
|
|
713
880
|
const filePath = (0, import_node_path4.join)(entityDir, filename);
|
|
714
881
|
if (atomicWrite(filePath, content)) {
|
|
@@ -1196,6 +1363,7 @@ var Lattice = class {
|
|
|
1196
1363
|
_renderHandlers = [];
|
|
1197
1364
|
_writebackHandlers = [];
|
|
1198
1365
|
_errorHandlers = [];
|
|
1366
|
+
_writeHooks = [];
|
|
1199
1367
|
constructor(pathOrConfig, options = {}) {
|
|
1200
1368
|
let dbPath;
|
|
1201
1369
|
let configTables;
|
|
@@ -1255,6 +1423,14 @@ var Lattice = class {
|
|
|
1255
1423
|
this._schema.defineEntityContext(table, def);
|
|
1256
1424
|
return this;
|
|
1257
1425
|
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Register a write hook that fires after insert/update/delete operations.
|
|
1428
|
+
* Hooks run synchronously after the DB write and audit emit.
|
|
1429
|
+
*/
|
|
1430
|
+
defineWriteHook(hook) {
|
|
1431
|
+
this._writeHooks.push(hook);
|
|
1432
|
+
return this;
|
|
1433
|
+
}
|
|
1258
1434
|
defineWriteback(def) {
|
|
1259
1435
|
this._writeback.define(def);
|
|
1260
1436
|
return this;
|
|
@@ -1304,6 +1480,7 @@ var Lattice = class {
|
|
|
1304
1480
|
const rawPk = rowWithPk[pkCol];
|
|
1305
1481
|
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
1306
1482
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
1483
|
+
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
1307
1484
|
return Promise.resolve(pkValue);
|
|
1308
1485
|
}
|
|
1309
1486
|
upsert(table, row) {
|
|
@@ -1357,6 +1534,7 @@ var Lattice = class {
|
|
|
1357
1534
|
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE ${clause}`, values);
|
|
1358
1535
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1359
1536
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
1537
|
+
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
1360
1538
|
return Promise.resolve();
|
|
1361
1539
|
}
|
|
1362
1540
|
delete(table, id) {
|
|
@@ -1366,6 +1544,7 @@ var Lattice = class {
|
|
|
1366
1544
|
this._adapter.run(`DELETE FROM "${table}" WHERE ${clause}`, params);
|
|
1367
1545
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1368
1546
|
this._sanitizer.emitAudit(table, "delete", auditId);
|
|
1547
|
+
this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
|
|
1369
1548
|
return Promise.resolve();
|
|
1370
1549
|
}
|
|
1371
1550
|
get(table, id) {
|
|
@@ -1603,6 +1782,22 @@ var Lattice = class {
|
|
|
1603
1782
|
return { clauses, params };
|
|
1604
1783
|
}
|
|
1605
1784
|
/** Returns a rejected Promise if not initialized; null if ready. */
|
|
1785
|
+
_fireWriteHooks(table, op, row, pk, changedColumns) {
|
|
1786
|
+
for (const hook of this._writeHooks) {
|
|
1787
|
+
if (hook.table !== table) continue;
|
|
1788
|
+
if (!hook.on.includes(op)) continue;
|
|
1789
|
+
if (op === "update" && hook.watchColumns && changedColumns) {
|
|
1790
|
+
if (!hook.watchColumns.some((c) => changedColumns.includes(c))) continue;
|
|
1791
|
+
}
|
|
1792
|
+
try {
|
|
1793
|
+
const ctx = { table, op, row, pk };
|
|
1794
|
+
if (changedColumns) ctx.changedColumns = changedColumns;
|
|
1795
|
+
hook.handler(ctx);
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
for (const h of this._errorHandlers) h(err instanceof Error ? err : new Error(String(err)));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1606
1801
|
_notInitError() {
|
|
1607
1802
|
if (!this._initialized) {
|
|
1608
1803
|
return Promise.reject(
|
|
@@ -1641,39 +1836,6 @@ var Lattice = class {
|
|
|
1641
1836
|
}
|
|
1642
1837
|
};
|
|
1643
1838
|
|
|
1644
|
-
// src/render/markdown.ts
|
|
1645
|
-
function frontmatter(fields) {
|
|
1646
|
-
const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
|
|
1647
|
-
for (const [key, val] of Object.entries(fields)) {
|
|
1648
|
-
lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
|
|
1649
|
-
}
|
|
1650
|
-
return `---
|
|
1651
|
-
${lines.join("\n")}
|
|
1652
|
-
---
|
|
1653
|
-
|
|
1654
|
-
`;
|
|
1655
|
-
}
|
|
1656
|
-
function markdownTable(rows, columns) {
|
|
1657
|
-
if (rows.length === 0 || columns.length === 0) return "";
|
|
1658
|
-
const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
|
|
1659
|
-
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
1660
|
-
const body = rows.map((row) => {
|
|
1661
|
-
const cells = columns.map((col) => {
|
|
1662
|
-
const raw = row[col.key];
|
|
1663
|
-
return col.format ? col.format(raw, row) : String(raw ?? "");
|
|
1664
|
-
});
|
|
1665
|
-
return "| " + cells.join(" | ") + " |";
|
|
1666
|
-
});
|
|
1667
|
-
return [header, separator, ...body].join("\n") + "\n";
|
|
1668
|
-
}
|
|
1669
|
-
function slugify(name) {
|
|
1670
|
-
return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1671
|
-
}
|
|
1672
|
-
function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
|
|
1673
|
-
if (content.length <= maxChars) return content;
|
|
1674
|
-
return content.slice(0, maxChars) + notice;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
1839
|
// src/session/parser.ts
|
|
1678
1840
|
var import_node_crypto3 = require("crypto");
|
|
1679
1841
|
function generateWriteEntryId(timestamp, agentName, op, table, target) {
|
|
@@ -2058,19 +2220,6 @@ function applyWriteEntry(db, entry) {
|
|
|
2058
2220
|
return { ok: false, reason: `DB error: ${message}` };
|
|
2059
2221
|
}
|
|
2060
2222
|
}
|
|
2061
|
-
|
|
2062
|
-
// src/session/constants.ts
|
|
2063
|
-
function createReadOnlyHeader(options) {
|
|
2064
|
-
const generator = options?.generator ?? "Lattice";
|
|
2065
|
-
const docsRef = options?.docsRef ?? "the Lattice documentation";
|
|
2066
|
-
return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
|
|
2067
|
-
To update data in Lattice: write entries to SESSION.md in this directory.
|
|
2068
|
-
Format: type: write | op: create/update/delete | table: <name> | target: <id>
|
|
2069
|
-
See ${docsRef} for the SESSION.md format spec. -->
|
|
2070
|
-
|
|
2071
|
-
`;
|
|
2072
|
-
}
|
|
2073
|
-
var READ_ONLY_HEADER = createReadOnlyHeader();
|
|
2074
2223
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2075
2224
|
0 && (module.exports = {
|
|
2076
2225
|
DEFAULT_ENTRY_TYPES,
|
package/dist/index.d.cts
CHANGED
|
@@ -266,14 +266,89 @@ type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsT
|
|
|
266
266
|
* }
|
|
267
267
|
* ```
|
|
268
268
|
*/
|
|
269
|
+
/** Column spec for entity-table template (reuses MarkdownTableColumn). */
|
|
270
|
+
interface EntityTableColumn {
|
|
271
|
+
key: string;
|
|
272
|
+
header: string;
|
|
273
|
+
format?: (val: unknown, row: Row) => string;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Render a heading + GFM table. Auto-prepends read-only header + frontmatter.
|
|
277
|
+
*/
|
|
278
|
+
interface EntityTableTemplate {
|
|
279
|
+
template: 'entity-table';
|
|
280
|
+
heading: string;
|
|
281
|
+
columns: EntityTableColumn[];
|
|
282
|
+
emptyMessage?: string;
|
|
283
|
+
frontmatter?: Record<string, string | number | boolean>;
|
|
284
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Field spec for entity-profile template.
|
|
288
|
+
*/
|
|
289
|
+
interface EntityProfileField {
|
|
290
|
+
key: string;
|
|
291
|
+
label: string;
|
|
292
|
+
format?: (val: unknown, row: Row) => string;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Section spec for entity-profile template (renders enriched JSON arrays).
|
|
296
|
+
*/
|
|
297
|
+
interface EntityProfileSection {
|
|
298
|
+
/** Key of the enriched field (e.g. 'agents' → reads row._agents). */
|
|
299
|
+
key: string;
|
|
300
|
+
heading: string | ((row: Row) => string);
|
|
301
|
+
condition?: (row: Row) => boolean;
|
|
302
|
+
render: 'table' | 'list' | ((items: Row[]) => string);
|
|
303
|
+
columns?: EntityTableColumn[];
|
|
304
|
+
formatItem?: (item: Row) => string;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Render entity profile: heading + field-value pairs + optional enriched sections.
|
|
308
|
+
*/
|
|
309
|
+
interface EntityProfileTemplate {
|
|
310
|
+
template: 'entity-profile';
|
|
311
|
+
heading: string | ((row: Row) => string);
|
|
312
|
+
fields: EntityProfileField[];
|
|
313
|
+
sections?: EntityProfileSection[];
|
|
314
|
+
frontmatter?: Record<string, string | number | boolean> | ((row: Row) => Record<string, string | number | boolean>);
|
|
315
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Per-row section spec for entity-sections template.
|
|
319
|
+
*/
|
|
320
|
+
interface EntitySectionPerRow {
|
|
321
|
+
heading: (row: Row) => string;
|
|
322
|
+
metadata?: Array<{
|
|
323
|
+
key: string;
|
|
324
|
+
label: string;
|
|
325
|
+
format?: (val: unknown) => string;
|
|
326
|
+
}>;
|
|
327
|
+
body?: (row: Row) => string;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Render per-row sections: heading + metadata key-value + body text per row.
|
|
331
|
+
*/
|
|
332
|
+
interface EntitySectionsTemplate {
|
|
333
|
+
template: 'entity-sections';
|
|
334
|
+
heading: string;
|
|
335
|
+
perRow: EntitySectionPerRow;
|
|
336
|
+
emptyMessage?: string;
|
|
337
|
+
frontmatter?: Record<string, string | number | boolean>;
|
|
338
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
339
|
+
}
|
|
340
|
+
/** Union of all entity render template types. */
|
|
341
|
+
type EntityRenderTemplate = EntityTableTemplate | EntityProfileTemplate | EntitySectionsTemplate;
|
|
342
|
+
/** Accepted values for EntityFileSpec.render — function or template object. */
|
|
343
|
+
type EntityRenderSpec = ((rows: Row[]) => string) | EntityRenderTemplate;
|
|
269
344
|
interface EntityFileSpec {
|
|
270
345
|
/** Determines what rows are passed to {@link render}. */
|
|
271
346
|
source: EntityFileSource;
|
|
272
347
|
/**
|
|
273
348
|
* Converts the resolved rows into the file's markdown content.
|
|
274
|
-
*
|
|
349
|
+
* Accepts a function `(rows) => string` or a declarative template object.
|
|
275
350
|
*/
|
|
276
|
-
render:
|
|
351
|
+
render: EntityRenderSpec;
|
|
277
352
|
/**
|
|
278
353
|
* Maximum number of characters allowed in the rendered output.
|
|
279
354
|
* Content exceeding this limit is truncated with a notice appended.
|
|
@@ -674,6 +749,45 @@ interface AuditEvent {
|
|
|
674
749
|
id: string;
|
|
675
750
|
timestamp: string;
|
|
676
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Context passed to write hook handlers.
|
|
754
|
+
*/
|
|
755
|
+
interface WriteHookContext {
|
|
756
|
+
/** Table that was modified. */
|
|
757
|
+
table: string;
|
|
758
|
+
/** The operation that triggered the hook. */
|
|
759
|
+
op: 'insert' | 'update' | 'delete';
|
|
760
|
+
/** The row data (for insert: full row; for update: changed fields; for delete: { id }). */
|
|
761
|
+
row: Row;
|
|
762
|
+
/** Primary key value(s) of the affected row. */
|
|
763
|
+
pk: string;
|
|
764
|
+
/** For updates: the column names that were changed. */
|
|
765
|
+
changedColumns?: string[];
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* A write hook fires after insert/update/delete operations.
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```ts
|
|
772
|
+
* db.defineWriteHook({
|
|
773
|
+
* table: 'agents',
|
|
774
|
+
* on: ['insert', 'update'],
|
|
775
|
+
* watchColumns: ['team_id'],
|
|
776
|
+
* handler: (ctx) => { denormalizeTeamFields(ctx.pk); },
|
|
777
|
+
* });
|
|
778
|
+
* ```
|
|
779
|
+
*/
|
|
780
|
+
interface WriteHook {
|
|
781
|
+
/** Table the hook fires on. */
|
|
782
|
+
table: string;
|
|
783
|
+
/** Operations that trigger the hook. */
|
|
784
|
+
on: Array<'insert' | 'update' | 'delete'>;
|
|
785
|
+
/** Only fire on update when these columns changed. Omit = fire on any change. */
|
|
786
|
+
watchColumns?: string[];
|
|
787
|
+
/** Handler function. Runs synchronously after the DB write. */
|
|
788
|
+
handler: (ctx: WriteHookContext) => void;
|
|
789
|
+
}
|
|
790
|
+
|
|
677
791
|
interface ReconcileOptions {
|
|
678
792
|
/** Remove entity directories whose slug is no longer in the DB. Default: true. */
|
|
679
793
|
removeOrphanedDirectories?: boolean;
|
|
@@ -726,10 +840,16 @@ declare class Lattice {
|
|
|
726
840
|
private readonly _renderHandlers;
|
|
727
841
|
private readonly _writebackHandlers;
|
|
728
842
|
private readonly _errorHandlers;
|
|
843
|
+
private readonly _writeHooks;
|
|
729
844
|
constructor(pathOrConfig: string | LatticeConfigInput, options?: LatticeOptions);
|
|
730
845
|
define(table: string, def: TableDefinition): this;
|
|
731
846
|
defineMulti(name: string, def: MultiTableDefinition): this;
|
|
732
847
|
defineEntityContext(table: string, def: EntityContextDefinition): this;
|
|
848
|
+
/**
|
|
849
|
+
* Register a write hook that fires after insert/update/delete operations.
|
|
850
|
+
* Hooks run synchronously after the DB write and audit emit.
|
|
851
|
+
*/
|
|
852
|
+
defineWriteHook(hook: WriteHook): this;
|
|
733
853
|
defineWriteback(def: WritebackDefinition): this;
|
|
734
854
|
init(options?: InitOptions): Promise<void>;
|
|
735
855
|
close(): void;
|
|
@@ -775,6 +895,7 @@ declare class Lattice {
|
|
|
775
895
|
*/
|
|
776
896
|
private _buildFilters;
|
|
777
897
|
/** Returns a rejected Promise if not initialized; null if ready. */
|
|
898
|
+
private _fireWriteHooks;
|
|
778
899
|
private _notInitError;
|
|
779
900
|
/**
|
|
780
901
|
* Returns a rejected Promise if any of the given column names are not present
|
|
@@ -1230,4 +1351,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
|
|
|
1230
1351
|
*/
|
|
1231
1352
|
declare const READ_ONLY_HEADER: string;
|
|
1232
1353
|
|
|
1233
|
-
export { type ApplyWriteResult, type AuditEvent, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileSource, type EntityFileSpec, type Filter, type FilterOp, type HasManyRelation, type HasManySource, type InitOptions, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type ManyToManySource, type MarkdownTableColumn, type Migration, type MultiTableDefinition, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, frontmatter, generateEntryId, generateWriteEntryId, manifestPath, markdownTable, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };
|
|
1354
|
+
export { type ApplyWriteResult, type AuditEvent, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, type InitOptions, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type ManyToManySource, type MarkdownTableColumn, type Migration, type MultiTableDefinition, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, frontmatter, generateEntryId, generateWriteEntryId, manifestPath, markdownTable, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };
|
package/dist/index.d.ts
CHANGED
|
@@ -266,14 +266,89 @@ type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsT
|
|
|
266
266
|
* }
|
|
267
267
|
* ```
|
|
268
268
|
*/
|
|
269
|
+
/** Column spec for entity-table template (reuses MarkdownTableColumn). */
|
|
270
|
+
interface EntityTableColumn {
|
|
271
|
+
key: string;
|
|
272
|
+
header: string;
|
|
273
|
+
format?: (val: unknown, row: Row) => string;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Render a heading + GFM table. Auto-prepends read-only header + frontmatter.
|
|
277
|
+
*/
|
|
278
|
+
interface EntityTableTemplate {
|
|
279
|
+
template: 'entity-table';
|
|
280
|
+
heading: string;
|
|
281
|
+
columns: EntityTableColumn[];
|
|
282
|
+
emptyMessage?: string;
|
|
283
|
+
frontmatter?: Record<string, string | number | boolean>;
|
|
284
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Field spec for entity-profile template.
|
|
288
|
+
*/
|
|
289
|
+
interface EntityProfileField {
|
|
290
|
+
key: string;
|
|
291
|
+
label: string;
|
|
292
|
+
format?: (val: unknown, row: Row) => string;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Section spec for entity-profile template (renders enriched JSON arrays).
|
|
296
|
+
*/
|
|
297
|
+
interface EntityProfileSection {
|
|
298
|
+
/** Key of the enriched field (e.g. 'agents' → reads row._agents). */
|
|
299
|
+
key: string;
|
|
300
|
+
heading: string | ((row: Row) => string);
|
|
301
|
+
condition?: (row: Row) => boolean;
|
|
302
|
+
render: 'table' | 'list' | ((items: Row[]) => string);
|
|
303
|
+
columns?: EntityTableColumn[];
|
|
304
|
+
formatItem?: (item: Row) => string;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Render entity profile: heading + field-value pairs + optional enriched sections.
|
|
308
|
+
*/
|
|
309
|
+
interface EntityProfileTemplate {
|
|
310
|
+
template: 'entity-profile';
|
|
311
|
+
heading: string | ((row: Row) => string);
|
|
312
|
+
fields: EntityProfileField[];
|
|
313
|
+
sections?: EntityProfileSection[];
|
|
314
|
+
frontmatter?: Record<string, string | number | boolean> | ((row: Row) => Record<string, string | number | boolean>);
|
|
315
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Per-row section spec for entity-sections template.
|
|
319
|
+
*/
|
|
320
|
+
interface EntitySectionPerRow {
|
|
321
|
+
heading: (row: Row) => string;
|
|
322
|
+
metadata?: Array<{
|
|
323
|
+
key: string;
|
|
324
|
+
label: string;
|
|
325
|
+
format?: (val: unknown) => string;
|
|
326
|
+
}>;
|
|
327
|
+
body?: (row: Row) => string;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Render per-row sections: heading + metadata key-value + body text per row.
|
|
331
|
+
*/
|
|
332
|
+
interface EntitySectionsTemplate {
|
|
333
|
+
template: 'entity-sections';
|
|
334
|
+
heading: string;
|
|
335
|
+
perRow: EntitySectionPerRow;
|
|
336
|
+
emptyMessage?: string;
|
|
337
|
+
frontmatter?: Record<string, string | number | boolean>;
|
|
338
|
+
beforeRender?: (rows: Row[]) => Row[];
|
|
339
|
+
}
|
|
340
|
+
/** Union of all entity render template types. */
|
|
341
|
+
type EntityRenderTemplate = EntityTableTemplate | EntityProfileTemplate | EntitySectionsTemplate;
|
|
342
|
+
/** Accepted values for EntityFileSpec.render — function or template object. */
|
|
343
|
+
type EntityRenderSpec = ((rows: Row[]) => string) | EntityRenderTemplate;
|
|
269
344
|
interface EntityFileSpec {
|
|
270
345
|
/** Determines what rows are passed to {@link render}. */
|
|
271
346
|
source: EntityFileSource;
|
|
272
347
|
/**
|
|
273
348
|
* Converts the resolved rows into the file's markdown content.
|
|
274
|
-
*
|
|
349
|
+
* Accepts a function `(rows) => string` or a declarative template object.
|
|
275
350
|
*/
|
|
276
|
-
render:
|
|
351
|
+
render: EntityRenderSpec;
|
|
277
352
|
/**
|
|
278
353
|
* Maximum number of characters allowed in the rendered output.
|
|
279
354
|
* Content exceeding this limit is truncated with a notice appended.
|
|
@@ -674,6 +749,45 @@ interface AuditEvent {
|
|
|
674
749
|
id: string;
|
|
675
750
|
timestamp: string;
|
|
676
751
|
}
|
|
752
|
+
/**
|
|
753
|
+
* Context passed to write hook handlers.
|
|
754
|
+
*/
|
|
755
|
+
interface WriteHookContext {
|
|
756
|
+
/** Table that was modified. */
|
|
757
|
+
table: string;
|
|
758
|
+
/** The operation that triggered the hook. */
|
|
759
|
+
op: 'insert' | 'update' | 'delete';
|
|
760
|
+
/** The row data (for insert: full row; for update: changed fields; for delete: { id }). */
|
|
761
|
+
row: Row;
|
|
762
|
+
/** Primary key value(s) of the affected row. */
|
|
763
|
+
pk: string;
|
|
764
|
+
/** For updates: the column names that were changed. */
|
|
765
|
+
changedColumns?: string[];
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* A write hook fires after insert/update/delete operations.
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```ts
|
|
772
|
+
* db.defineWriteHook({
|
|
773
|
+
* table: 'agents',
|
|
774
|
+
* on: ['insert', 'update'],
|
|
775
|
+
* watchColumns: ['team_id'],
|
|
776
|
+
* handler: (ctx) => { denormalizeTeamFields(ctx.pk); },
|
|
777
|
+
* });
|
|
778
|
+
* ```
|
|
779
|
+
*/
|
|
780
|
+
interface WriteHook {
|
|
781
|
+
/** Table the hook fires on. */
|
|
782
|
+
table: string;
|
|
783
|
+
/** Operations that trigger the hook. */
|
|
784
|
+
on: Array<'insert' | 'update' | 'delete'>;
|
|
785
|
+
/** Only fire on update when these columns changed. Omit = fire on any change. */
|
|
786
|
+
watchColumns?: string[];
|
|
787
|
+
/** Handler function. Runs synchronously after the DB write. */
|
|
788
|
+
handler: (ctx: WriteHookContext) => void;
|
|
789
|
+
}
|
|
790
|
+
|
|
677
791
|
interface ReconcileOptions {
|
|
678
792
|
/** Remove entity directories whose slug is no longer in the DB. Default: true. */
|
|
679
793
|
removeOrphanedDirectories?: boolean;
|
|
@@ -726,10 +840,16 @@ declare class Lattice {
|
|
|
726
840
|
private readonly _renderHandlers;
|
|
727
841
|
private readonly _writebackHandlers;
|
|
728
842
|
private readonly _errorHandlers;
|
|
843
|
+
private readonly _writeHooks;
|
|
729
844
|
constructor(pathOrConfig: string | LatticeConfigInput, options?: LatticeOptions);
|
|
730
845
|
define(table: string, def: TableDefinition): this;
|
|
731
846
|
defineMulti(name: string, def: MultiTableDefinition): this;
|
|
732
847
|
defineEntityContext(table: string, def: EntityContextDefinition): this;
|
|
848
|
+
/**
|
|
849
|
+
* Register a write hook that fires after insert/update/delete operations.
|
|
850
|
+
* Hooks run synchronously after the DB write and audit emit.
|
|
851
|
+
*/
|
|
852
|
+
defineWriteHook(hook: WriteHook): this;
|
|
733
853
|
defineWriteback(def: WritebackDefinition): this;
|
|
734
854
|
init(options?: InitOptions): Promise<void>;
|
|
735
855
|
close(): void;
|
|
@@ -775,6 +895,7 @@ declare class Lattice {
|
|
|
775
895
|
*/
|
|
776
896
|
private _buildFilters;
|
|
777
897
|
/** Returns a rejected Promise if not initialized; null if ready. */
|
|
898
|
+
private _fireWriteHooks;
|
|
778
899
|
private _notInitError;
|
|
779
900
|
/**
|
|
780
901
|
* Returns a rejected Promise if any of the given column names are not present
|
|
@@ -1230,4 +1351,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
|
|
|
1230
1351
|
*/
|
|
1231
1352
|
declare const READ_ONLY_HEADER: string;
|
|
1232
1353
|
|
|
1233
|
-
export { type ApplyWriteResult, type AuditEvent, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileSource, type EntityFileSpec, type Filter, type FilterOp, type HasManyRelation, type HasManySource, type InitOptions, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type ManyToManySource, type MarkdownTableColumn, type Migration, type MultiTableDefinition, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, frontmatter, generateEntryId, generateWriteEntryId, manifestPath, markdownTable, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };
|
|
1354
|
+
export { type ApplyWriteResult, type AuditEvent, type BelongsToRelation, type BelongsToSource, type BuiltinTemplateName, type CleanupOptions, type CleanupResult, type CountOptions, type CustomSource, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES, type EnrichedSource, type EnrichmentLookup, type EntityContextDefinition, type EntityContextManifestEntry, type EntityFileSource, type EntityFileSpec, type EntityProfileField, type EntityProfileSection, type EntityProfileTemplate, type EntityRenderSpec, type EntityRenderTemplate, type EntitySectionPerRow, type EntitySectionsTemplate, type EntityTableColumn, type EntityTableTemplate, type Filter, type FilterOp, type HasManyRelation, type HasManySource, type InitOptions, Lattice, type LatticeConfig, type LatticeConfigInput, type LatticeEntityDef, type LatticeEntityRenderSpec, type LatticeFieldDef, type LatticeFieldType, type LatticeManifest, type LatticeOptions, type ManyToManySource, type MarkdownTableColumn, type Migration, type MultiTableDefinition, type OrderBySpec, type ParseError, type ParseResult, type ParsedConfig, type PkLookup, type PrimaryKey, type QueryOptions, READ_ONLY_HEADER, type ReadOnlyHeaderOptions, type ReconcileOptions, type ReconcileResult, type Relation, type RenderHooks, type RenderResult, type RenderSpec, type Row, type SecurityOptions, type SelfSource, type SessionEntry, type SessionParseOptions, type SessionWriteEntry, type SessionWriteOp, type SessionWriteParseResult, type SourceQueryOptions, type StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WriteHook, type WriteHookContext, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, frontmatter, generateEntryId, generateWriteEntryId, manifestPath, markdownTable, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, slugify, truncate, validateEntryId, writeManifest };
|
package/dist/index.js
CHANGED
|
@@ -443,6 +443,172 @@ function truncateContent(content, budget) {
|
|
|
443
443
|
return content.slice(0, budget) + "\n\n*[truncated \u2014 context budget exceeded]*";
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
// src/render/markdown.ts
|
|
447
|
+
function frontmatter(fields) {
|
|
448
|
+
const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
|
|
449
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
450
|
+
lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
|
|
451
|
+
}
|
|
452
|
+
return `---
|
|
453
|
+
${lines.join("\n")}
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
function markdownTable(rows, columns) {
|
|
459
|
+
if (rows.length === 0 || columns.length === 0) return "";
|
|
460
|
+
const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
|
|
461
|
+
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
462
|
+
const body = rows.map((row) => {
|
|
463
|
+
const cells = columns.map((col) => {
|
|
464
|
+
const raw = row[col.key];
|
|
465
|
+
return col.format ? col.format(raw, row) : String(raw ?? "");
|
|
466
|
+
});
|
|
467
|
+
return "| " + cells.join(" | ") + " |";
|
|
468
|
+
});
|
|
469
|
+
return [header, separator, ...body].join("\n") + "\n";
|
|
470
|
+
}
|
|
471
|
+
function slugify(name) {
|
|
472
|
+
return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
473
|
+
}
|
|
474
|
+
function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
|
|
475
|
+
if (content.length <= maxChars) return content;
|
|
476
|
+
return content.slice(0, maxChars) + notice;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/session/constants.ts
|
|
480
|
+
function createReadOnlyHeader(options) {
|
|
481
|
+
const generator = options?.generator ?? "Lattice";
|
|
482
|
+
const docsRef = options?.docsRef ?? "the Lattice documentation";
|
|
483
|
+
return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
|
|
484
|
+
To update data in Lattice: write entries to SESSION.md in this directory.
|
|
485
|
+
Format: type: write | op: create/update/delete | table: <name> | target: <id>
|
|
486
|
+
See ${docsRef} for the SESSION.md format spec. -->
|
|
487
|
+
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
var READ_ONLY_HEADER = createReadOnlyHeader();
|
|
491
|
+
|
|
492
|
+
// src/render/entity-templates.ts
|
|
493
|
+
var DEFAULT_HEADER = createReadOnlyHeader();
|
|
494
|
+
function compileEntityRender(spec) {
|
|
495
|
+
if (typeof spec === "function") return spec;
|
|
496
|
+
return compileTemplate(spec);
|
|
497
|
+
}
|
|
498
|
+
function compileTemplate(tmpl) {
|
|
499
|
+
switch (tmpl.template) {
|
|
500
|
+
case "entity-table":
|
|
501
|
+
return compileEntityTable(tmpl);
|
|
502
|
+
case "entity-profile":
|
|
503
|
+
return compileEntityProfile(tmpl);
|
|
504
|
+
case "entity-sections":
|
|
505
|
+
return compileEntitySections(tmpl);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function compileEntityTable(tmpl) {
|
|
509
|
+
return (rows) => {
|
|
510
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
511
|
+
let md = DEFAULT_HEADER;
|
|
512
|
+
md += frontmatter(tmpl.frontmatter ?? {});
|
|
513
|
+
md += `# ${tmpl.heading}
|
|
514
|
+
|
|
515
|
+
`;
|
|
516
|
+
if (data.length === 0) {
|
|
517
|
+
md += tmpl.emptyMessage ?? "*No data.*\n";
|
|
518
|
+
} else {
|
|
519
|
+
md += markdownTable(data, tmpl.columns);
|
|
520
|
+
}
|
|
521
|
+
return md;
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function compileEntityProfile(tmpl) {
|
|
525
|
+
return (rows) => {
|
|
526
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
527
|
+
const r = data[0];
|
|
528
|
+
if (!r) return "";
|
|
529
|
+
let md = DEFAULT_HEADER;
|
|
530
|
+
if (tmpl.frontmatter) {
|
|
531
|
+
const fm = typeof tmpl.frontmatter === "function" ? tmpl.frontmatter(r) : tmpl.frontmatter;
|
|
532
|
+
md += frontmatter(fm);
|
|
533
|
+
} else {
|
|
534
|
+
md += frontmatter({});
|
|
535
|
+
}
|
|
536
|
+
const heading = typeof tmpl.heading === "function" ? tmpl.heading(r) : tmpl.heading;
|
|
537
|
+
md += `# ${heading}
|
|
538
|
+
|
|
539
|
+
`;
|
|
540
|
+
for (const field of tmpl.fields) {
|
|
541
|
+
const val = r[field.key];
|
|
542
|
+
if (val === null || val === void 0) continue;
|
|
543
|
+
const formatted = field.format ? field.format(val, r) : String(val);
|
|
544
|
+
if (formatted) {
|
|
545
|
+
md += `**${field.label}:** ${formatted}
|
|
546
|
+
`;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (tmpl.sections) {
|
|
550
|
+
for (const section of tmpl.sections) {
|
|
551
|
+
const rawJson = r[`_${section.key}`];
|
|
552
|
+
if (!rawJson) continue;
|
|
553
|
+
if (section.condition && !section.condition(r)) continue;
|
|
554
|
+
const items = JSON.parse(rawJson);
|
|
555
|
+
if (items.length === 0) continue;
|
|
556
|
+
const sectionHeading = typeof section.heading === "function" ? section.heading(r) : section.heading;
|
|
557
|
+
md += `
|
|
558
|
+
## ${sectionHeading}
|
|
559
|
+
|
|
560
|
+
`;
|
|
561
|
+
if (section.render === "table" && section.columns) {
|
|
562
|
+
md += markdownTable(items, section.columns);
|
|
563
|
+
} else if (section.render === "list" && section.formatItem) {
|
|
564
|
+
for (const item of items) {
|
|
565
|
+
md += `- ${section.formatItem(item)}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
} else if (typeof section.render === "function") {
|
|
569
|
+
md += section.render(items);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return md;
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function compileEntitySections(tmpl) {
|
|
577
|
+
return (rows) => {
|
|
578
|
+
const data = tmpl.beforeRender ? tmpl.beforeRender(rows) : rows;
|
|
579
|
+
let md = DEFAULT_HEADER;
|
|
580
|
+
md += frontmatter(tmpl.frontmatter ?? {});
|
|
581
|
+
md += `# ${tmpl.heading}
|
|
582
|
+
|
|
583
|
+
`;
|
|
584
|
+
if (data.length === 0) {
|
|
585
|
+
md += tmpl.emptyMessage ?? "*No data.*\n";
|
|
586
|
+
return md;
|
|
587
|
+
}
|
|
588
|
+
for (const row of data) {
|
|
589
|
+
md += `## ${tmpl.perRow.heading(row)}
|
|
590
|
+
`;
|
|
591
|
+
if (tmpl.perRow.metadata?.length) {
|
|
592
|
+
const parts = tmpl.perRow.metadata.map((m) => {
|
|
593
|
+
const val = row[m.key];
|
|
594
|
+
const formatted = m.format ? m.format(val) : String(val ?? "");
|
|
595
|
+
return `**${m.label}:** ${formatted}`;
|
|
596
|
+
}).filter(Boolean);
|
|
597
|
+
if (parts.length > 0) {
|
|
598
|
+
md += parts.join(" | ") + "\n";
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (tmpl.perRow.body) {
|
|
602
|
+
md += `
|
|
603
|
+
${tmpl.perRow.body(row)}
|
|
604
|
+
`;
|
|
605
|
+
}
|
|
606
|
+
md += "\n";
|
|
607
|
+
}
|
|
608
|
+
return md;
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
446
612
|
// src/lifecycle/cleanup.ts
|
|
447
613
|
import { join as join3 } from "path";
|
|
448
614
|
import { existsSync as existsSync3, readdirSync, unlinkSync, rmdirSync, statSync } from "fs";
|
|
@@ -652,7 +818,8 @@ var RenderEngine = class {
|
|
|
652
818
|
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
653
819
|
const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
|
|
654
820
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
655
|
-
const
|
|
821
|
+
const renderFn = compileEntityRender(spec.render);
|
|
822
|
+
const content = truncateContent(renderFn(rows), spec.budget);
|
|
656
823
|
renderedFiles.set(filename, content);
|
|
657
824
|
const filePath = join4(entityDir, filename);
|
|
658
825
|
if (atomicWrite(filePath, content)) {
|
|
@@ -1140,6 +1307,7 @@ var Lattice = class {
|
|
|
1140
1307
|
_renderHandlers = [];
|
|
1141
1308
|
_writebackHandlers = [];
|
|
1142
1309
|
_errorHandlers = [];
|
|
1310
|
+
_writeHooks = [];
|
|
1143
1311
|
constructor(pathOrConfig, options = {}) {
|
|
1144
1312
|
let dbPath;
|
|
1145
1313
|
let configTables;
|
|
@@ -1199,6 +1367,14 @@ var Lattice = class {
|
|
|
1199
1367
|
this._schema.defineEntityContext(table, def);
|
|
1200
1368
|
return this;
|
|
1201
1369
|
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Register a write hook that fires after insert/update/delete operations.
|
|
1372
|
+
* Hooks run synchronously after the DB write and audit emit.
|
|
1373
|
+
*/
|
|
1374
|
+
defineWriteHook(hook) {
|
|
1375
|
+
this._writeHooks.push(hook);
|
|
1376
|
+
return this;
|
|
1377
|
+
}
|
|
1202
1378
|
defineWriteback(def) {
|
|
1203
1379
|
this._writeback.define(def);
|
|
1204
1380
|
return this;
|
|
@@ -1248,6 +1424,7 @@ var Lattice = class {
|
|
|
1248
1424
|
const rawPk = rowWithPk[pkCol];
|
|
1249
1425
|
const pkValue = rawPk != null ? String(rawPk) : "";
|
|
1250
1426
|
this._sanitizer.emitAudit(table, "insert", pkValue);
|
|
1427
|
+
this._fireWriteHooks(table, "insert", rowWithPk, pkValue);
|
|
1251
1428
|
return Promise.resolve(pkValue);
|
|
1252
1429
|
}
|
|
1253
1430
|
upsert(table, row) {
|
|
@@ -1301,6 +1478,7 @@ var Lattice = class {
|
|
|
1301
1478
|
this._adapter.run(`UPDATE "${table}" SET ${setCols} WHERE ${clause}`, values);
|
|
1302
1479
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1303
1480
|
this._sanitizer.emitAudit(table, "update", auditId);
|
|
1481
|
+
this._fireWriteHooks(table, "update", sanitized, auditId, Object.keys(sanitized));
|
|
1304
1482
|
return Promise.resolve();
|
|
1305
1483
|
}
|
|
1306
1484
|
delete(table, id) {
|
|
@@ -1310,6 +1488,7 @@ var Lattice = class {
|
|
|
1310
1488
|
this._adapter.run(`DELETE FROM "${table}" WHERE ${clause}`, params);
|
|
1311
1489
|
const auditId = typeof id === "string" ? id : JSON.stringify(id);
|
|
1312
1490
|
this._sanitizer.emitAudit(table, "delete", auditId);
|
|
1491
|
+
this._fireWriteHooks(table, "delete", { id: auditId }, auditId);
|
|
1313
1492
|
return Promise.resolve();
|
|
1314
1493
|
}
|
|
1315
1494
|
get(table, id) {
|
|
@@ -1547,6 +1726,22 @@ var Lattice = class {
|
|
|
1547
1726
|
return { clauses, params };
|
|
1548
1727
|
}
|
|
1549
1728
|
/** Returns a rejected Promise if not initialized; null if ready. */
|
|
1729
|
+
_fireWriteHooks(table, op, row, pk, changedColumns) {
|
|
1730
|
+
for (const hook of this._writeHooks) {
|
|
1731
|
+
if (hook.table !== table) continue;
|
|
1732
|
+
if (!hook.on.includes(op)) continue;
|
|
1733
|
+
if (op === "update" && hook.watchColumns && changedColumns) {
|
|
1734
|
+
if (!hook.watchColumns.some((c) => changedColumns.includes(c))) continue;
|
|
1735
|
+
}
|
|
1736
|
+
try {
|
|
1737
|
+
const ctx = { table, op, row, pk };
|
|
1738
|
+
if (changedColumns) ctx.changedColumns = changedColumns;
|
|
1739
|
+
hook.handler(ctx);
|
|
1740
|
+
} catch (err) {
|
|
1741
|
+
for (const h of this._errorHandlers) h(err instanceof Error ? err : new Error(String(err)));
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1550
1745
|
_notInitError() {
|
|
1551
1746
|
if (!this._initialized) {
|
|
1552
1747
|
return Promise.reject(
|
|
@@ -1585,39 +1780,6 @@ var Lattice = class {
|
|
|
1585
1780
|
}
|
|
1586
1781
|
};
|
|
1587
1782
|
|
|
1588
|
-
// src/render/markdown.ts
|
|
1589
|
-
function frontmatter(fields) {
|
|
1590
|
-
const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
|
|
1591
|
-
for (const [key, val] of Object.entries(fields)) {
|
|
1592
|
-
lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
|
|
1593
|
-
}
|
|
1594
|
-
return `---
|
|
1595
|
-
${lines.join("\n")}
|
|
1596
|
-
---
|
|
1597
|
-
|
|
1598
|
-
`;
|
|
1599
|
-
}
|
|
1600
|
-
function markdownTable(rows, columns) {
|
|
1601
|
-
if (rows.length === 0 || columns.length === 0) return "";
|
|
1602
|
-
const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
|
|
1603
|
-
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
1604
|
-
const body = rows.map((row) => {
|
|
1605
|
-
const cells = columns.map((col) => {
|
|
1606
|
-
const raw = row[col.key];
|
|
1607
|
-
return col.format ? col.format(raw, row) : String(raw ?? "");
|
|
1608
|
-
});
|
|
1609
|
-
return "| " + cells.join(" | ") + " |";
|
|
1610
|
-
});
|
|
1611
|
-
return [header, separator, ...body].join("\n") + "\n";
|
|
1612
|
-
}
|
|
1613
|
-
function slugify(name) {
|
|
1614
|
-
return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1615
|
-
}
|
|
1616
|
-
function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
|
|
1617
|
-
if (content.length <= maxChars) return content;
|
|
1618
|
-
return content.slice(0, maxChars) + notice;
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
1783
|
// src/session/parser.ts
|
|
1622
1784
|
import { createHash as createHash2 } from "crypto";
|
|
1623
1785
|
function generateWriteEntryId(timestamp, agentName, op, table, target) {
|
|
@@ -2002,19 +2164,6 @@ function applyWriteEntry(db, entry) {
|
|
|
2002
2164
|
return { ok: false, reason: `DB error: ${message}` };
|
|
2003
2165
|
}
|
|
2004
2166
|
}
|
|
2005
|
-
|
|
2006
|
-
// src/session/constants.ts
|
|
2007
|
-
function createReadOnlyHeader(options) {
|
|
2008
|
-
const generator = options?.generator ?? "Lattice";
|
|
2009
|
-
const docsRef = options?.docsRef ?? "the Lattice documentation";
|
|
2010
|
-
return `<!-- READ ONLY \u2014 generated by ${generator}. Do not edit directly.
|
|
2011
|
-
To update data in Lattice: write entries to SESSION.md in this directory.
|
|
2012
|
-
Format: type: write | op: create/update/delete | table: <name> | target: <id>
|
|
2013
|
-
See ${docsRef} for the SESSION.md format spec. -->
|
|
2014
|
-
|
|
2015
|
-
`;
|
|
2016
|
-
}
|
|
2017
|
-
var READ_ONLY_HEADER = createReadOnlyHeader();
|
|
2018
2167
|
export {
|
|
2019
2168
|
DEFAULT_ENTRY_TYPES,
|
|
2020
2169
|
DEFAULT_TYPE_ALIASES,
|