latticesql 0.5.5 → 0.7.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 +107 -15
- package/dist/index.cjs +148 -15
- package/dist/index.d.cts +154 -5
- package/dist/index.d.ts +154 -5
- package/dist/index.js +144 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -606,6 +606,75 @@ import { join as join5 } from "path";
|
|
|
606
606
|
import { mkdirSync as mkdirSync3 } from "fs";
|
|
607
607
|
|
|
608
608
|
// src/render/entity-query.ts
|
|
609
|
+
var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
|
|
610
|
+
function effectiveFilters(opts) {
|
|
611
|
+
const filters = opts.filters ? [...opts.filters] : [];
|
|
612
|
+
if (opts.softDelete && !filters.some((f) => f.col === "deleted_at")) {
|
|
613
|
+
filters.unshift({ col: "deleted_at", op: "isNull" });
|
|
614
|
+
}
|
|
615
|
+
return filters;
|
|
616
|
+
}
|
|
617
|
+
function appendQueryOptions(baseSql, params, opts, tableAlias) {
|
|
618
|
+
let sql = baseSql;
|
|
619
|
+
const prefix = tableAlias ? `${tableAlias}.` : "";
|
|
620
|
+
for (const f of effectiveFilters(opts)) {
|
|
621
|
+
if (!SAFE_COL_RE.test(f.col)) continue;
|
|
622
|
+
switch (f.op) {
|
|
623
|
+
case "eq":
|
|
624
|
+
sql += ` AND ${prefix}"${f.col}" = ?`;
|
|
625
|
+
params.push(f.val);
|
|
626
|
+
break;
|
|
627
|
+
case "ne":
|
|
628
|
+
sql += ` AND ${prefix}"${f.col}" != ?`;
|
|
629
|
+
params.push(f.val);
|
|
630
|
+
break;
|
|
631
|
+
case "gt":
|
|
632
|
+
sql += ` AND ${prefix}"${f.col}" > ?`;
|
|
633
|
+
params.push(f.val);
|
|
634
|
+
break;
|
|
635
|
+
case "gte":
|
|
636
|
+
sql += ` AND ${prefix}"${f.col}" >= ?`;
|
|
637
|
+
params.push(f.val);
|
|
638
|
+
break;
|
|
639
|
+
case "lt":
|
|
640
|
+
sql += ` AND ${prefix}"${f.col}" < ?`;
|
|
641
|
+
params.push(f.val);
|
|
642
|
+
break;
|
|
643
|
+
case "lte":
|
|
644
|
+
sql += ` AND ${prefix}"${f.col}" <= ?`;
|
|
645
|
+
params.push(f.val);
|
|
646
|
+
break;
|
|
647
|
+
case "like":
|
|
648
|
+
sql += ` AND ${prefix}"${f.col}" LIKE ?`;
|
|
649
|
+
params.push(f.val);
|
|
650
|
+
break;
|
|
651
|
+
case "in": {
|
|
652
|
+
const arr = f.val;
|
|
653
|
+
if (arr.length === 0) {
|
|
654
|
+
sql += " AND 0";
|
|
655
|
+
} else {
|
|
656
|
+
sql += ` AND ${prefix}"${f.col}" IN (${arr.map(() => "?").join(", ")})`;
|
|
657
|
+
params.push(...arr);
|
|
658
|
+
}
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
case "isNull":
|
|
662
|
+
sql += ` AND ${prefix}"${f.col}" IS NULL`;
|
|
663
|
+
break;
|
|
664
|
+
case "isNotNull":
|
|
665
|
+
sql += ` AND ${prefix}"${f.col}" IS NOT NULL`;
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (opts.orderBy && SAFE_COL_RE.test(opts.orderBy)) {
|
|
670
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
671
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
672
|
+
}
|
|
673
|
+
if (opts.limit !== void 0 && opts.limit > 0) {
|
|
674
|
+
sql += ` LIMIT ${Math.floor(opts.limit)}`;
|
|
675
|
+
}
|
|
676
|
+
return sql;
|
|
677
|
+
}
|
|
609
678
|
function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
610
679
|
switch (source.type) {
|
|
611
680
|
case "self":
|
|
@@ -613,32 +682,53 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
|
613
682
|
case "hasMany": {
|
|
614
683
|
const ref = source.references ?? entityPk;
|
|
615
684
|
const pkVal = entityRow[ref];
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
);
|
|
685
|
+
const params = [pkVal];
|
|
686
|
+
let sql = `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`;
|
|
687
|
+
sql = appendQueryOptions(sql, params, source);
|
|
688
|
+
return adapter.all(sql, params);
|
|
620
689
|
}
|
|
621
690
|
case "manyToMany": {
|
|
622
691
|
const pkVal = entityRow[entityPk];
|
|
623
692
|
const remotePk = source.references ?? "id";
|
|
624
|
-
|
|
625
|
-
|
|
693
|
+
const params = [pkVal];
|
|
694
|
+
let sql = `SELECT r.* FROM "${source.remoteTable}" r
|
|
626
695
|
JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
|
|
627
|
-
WHERE j."${source.localKey}" =
|
|
628
|
-
|
|
629
|
-
);
|
|
696
|
+
WHERE j."${source.localKey}" = ?`;
|
|
697
|
+
sql = appendQueryOptions(sql, params, source, "r");
|
|
698
|
+
return adapter.all(sql, params);
|
|
630
699
|
}
|
|
631
700
|
case "belongsTo": {
|
|
632
701
|
const fkVal = entityRow[source.foreignKey];
|
|
633
702
|
if (fkVal == null) return [];
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
703
|
+
const hasOptions = source.filters?.length || source.softDelete || source.orderBy || source.limit;
|
|
704
|
+
if (!hasOptions) {
|
|
705
|
+
const related = adapter.get(
|
|
706
|
+
`SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
|
|
707
|
+
[fkVal]
|
|
708
|
+
);
|
|
709
|
+
return related ? [related] : [];
|
|
710
|
+
}
|
|
711
|
+
const params = [fkVal];
|
|
712
|
+
let sql = `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`;
|
|
713
|
+
sql = appendQueryOptions(sql, params, source);
|
|
714
|
+
const rows = adapter.all(sql, params);
|
|
715
|
+
return rows.length > 0 ? [rows[0]] : [];
|
|
639
716
|
}
|
|
640
717
|
case "custom":
|
|
641
718
|
return source.query(entityRow, adapter);
|
|
719
|
+
case "enriched": {
|
|
720
|
+
const enriched = { ...entityRow };
|
|
721
|
+
for (const [key, lookup] of Object.entries(source.include)) {
|
|
722
|
+
const fieldName = `_${key}`;
|
|
723
|
+
if (lookup.type === "custom") {
|
|
724
|
+
enriched[fieldName] = JSON.stringify(lookup.query(entityRow, adapter));
|
|
725
|
+
} else {
|
|
726
|
+
const resolved = resolveEntitySource(lookup, entityRow, entityPk, adapter);
|
|
727
|
+
enriched[fieldName] = JSON.stringify(resolved);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return [enriched];
|
|
731
|
+
}
|
|
642
732
|
}
|
|
643
733
|
}
|
|
644
734
|
function truncateContent(content, budget) {
|
|
@@ -851,7 +941,9 @@ var RenderEngine = class {
|
|
|
851
941
|
mkdirSync3(entityDir, { recursive: true });
|
|
852
942
|
const renderedFiles = /* @__PURE__ */ new Map();
|
|
853
943
|
for (const [filename, spec] of Object.entries(def.files)) {
|
|
854
|
-
const
|
|
944
|
+
const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
|
|
945
|
+
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
946
|
+
const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
|
|
855
947
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
856
948
|
const content = truncateContent(spec.render(rows), spec.budget);
|
|
857
949
|
renderedFiles.set(filename, content);
|
package/dist/index.cjs
CHANGED
|
@@ -36,15 +36,19 @@ __export(index_exports, {
|
|
|
36
36
|
READ_ONLY_HEADER: () => READ_ONLY_HEADER,
|
|
37
37
|
applyWriteEntry: () => applyWriteEntry,
|
|
38
38
|
createReadOnlyHeader: () => createReadOnlyHeader,
|
|
39
|
+
frontmatter: () => frontmatter,
|
|
39
40
|
generateEntryId: () => generateEntryId,
|
|
40
41
|
generateWriteEntryId: () => generateWriteEntryId,
|
|
41
42
|
manifestPath: () => manifestPath,
|
|
43
|
+
markdownTable: () => markdownTable,
|
|
42
44
|
parseConfigFile: () => parseConfigFile,
|
|
43
45
|
parseConfigString: () => parseConfigString,
|
|
44
46
|
parseMarkdownEntries: () => parseMarkdownEntries,
|
|
45
47
|
parseSessionMD: () => parseSessionMD,
|
|
46
48
|
parseSessionWrites: () => parseSessionWrites,
|
|
47
49
|
readManifest: () => readManifest,
|
|
50
|
+
slugify: () => slugify,
|
|
51
|
+
truncate: () => truncate,
|
|
48
52
|
validateEntryId: () => validateEntryId,
|
|
49
53
|
writeManifest: () => writeManifest
|
|
50
54
|
});
|
|
@@ -344,6 +348,75 @@ var import_node_path4 = require("path");
|
|
|
344
348
|
var import_node_fs4 = require("fs");
|
|
345
349
|
|
|
346
350
|
// src/render/entity-query.ts
|
|
351
|
+
var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
|
|
352
|
+
function effectiveFilters(opts) {
|
|
353
|
+
const filters = opts.filters ? [...opts.filters] : [];
|
|
354
|
+
if (opts.softDelete && !filters.some((f) => f.col === "deleted_at")) {
|
|
355
|
+
filters.unshift({ col: "deleted_at", op: "isNull" });
|
|
356
|
+
}
|
|
357
|
+
return filters;
|
|
358
|
+
}
|
|
359
|
+
function appendQueryOptions(baseSql, params, opts, tableAlias) {
|
|
360
|
+
let sql = baseSql;
|
|
361
|
+
const prefix = tableAlias ? `${tableAlias}.` : "";
|
|
362
|
+
for (const f of effectiveFilters(opts)) {
|
|
363
|
+
if (!SAFE_COL_RE.test(f.col)) continue;
|
|
364
|
+
switch (f.op) {
|
|
365
|
+
case "eq":
|
|
366
|
+
sql += ` AND ${prefix}"${f.col}" = ?`;
|
|
367
|
+
params.push(f.val);
|
|
368
|
+
break;
|
|
369
|
+
case "ne":
|
|
370
|
+
sql += ` AND ${prefix}"${f.col}" != ?`;
|
|
371
|
+
params.push(f.val);
|
|
372
|
+
break;
|
|
373
|
+
case "gt":
|
|
374
|
+
sql += ` AND ${prefix}"${f.col}" > ?`;
|
|
375
|
+
params.push(f.val);
|
|
376
|
+
break;
|
|
377
|
+
case "gte":
|
|
378
|
+
sql += ` AND ${prefix}"${f.col}" >= ?`;
|
|
379
|
+
params.push(f.val);
|
|
380
|
+
break;
|
|
381
|
+
case "lt":
|
|
382
|
+
sql += ` AND ${prefix}"${f.col}" < ?`;
|
|
383
|
+
params.push(f.val);
|
|
384
|
+
break;
|
|
385
|
+
case "lte":
|
|
386
|
+
sql += ` AND ${prefix}"${f.col}" <= ?`;
|
|
387
|
+
params.push(f.val);
|
|
388
|
+
break;
|
|
389
|
+
case "like":
|
|
390
|
+
sql += ` AND ${prefix}"${f.col}" LIKE ?`;
|
|
391
|
+
params.push(f.val);
|
|
392
|
+
break;
|
|
393
|
+
case "in": {
|
|
394
|
+
const arr = f.val;
|
|
395
|
+
if (arr.length === 0) {
|
|
396
|
+
sql += " AND 0";
|
|
397
|
+
} else {
|
|
398
|
+
sql += ` AND ${prefix}"${f.col}" IN (${arr.map(() => "?").join(", ")})`;
|
|
399
|
+
params.push(...arr);
|
|
400
|
+
}
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
case "isNull":
|
|
404
|
+
sql += ` AND ${prefix}"${f.col}" IS NULL`;
|
|
405
|
+
break;
|
|
406
|
+
case "isNotNull":
|
|
407
|
+
sql += ` AND ${prefix}"${f.col}" IS NOT NULL`;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (opts.orderBy && SAFE_COL_RE.test(opts.orderBy)) {
|
|
412
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
413
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
414
|
+
}
|
|
415
|
+
if (opts.limit !== void 0 && opts.limit > 0) {
|
|
416
|
+
sql += ` LIMIT ${Math.floor(opts.limit)}`;
|
|
417
|
+
}
|
|
418
|
+
return sql;
|
|
419
|
+
}
|
|
347
420
|
function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
348
421
|
switch (source.type) {
|
|
349
422
|
case "self":
|
|
@@ -351,32 +424,53 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
|
351
424
|
case "hasMany": {
|
|
352
425
|
const ref = source.references ?? entityPk;
|
|
353
426
|
const pkVal = entityRow[ref];
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
);
|
|
427
|
+
const params = [pkVal];
|
|
428
|
+
let sql = `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`;
|
|
429
|
+
sql = appendQueryOptions(sql, params, source);
|
|
430
|
+
return adapter.all(sql, params);
|
|
358
431
|
}
|
|
359
432
|
case "manyToMany": {
|
|
360
433
|
const pkVal = entityRow[entityPk];
|
|
361
434
|
const remotePk = source.references ?? "id";
|
|
362
|
-
|
|
363
|
-
|
|
435
|
+
const params = [pkVal];
|
|
436
|
+
let sql = `SELECT r.* FROM "${source.remoteTable}" r
|
|
364
437
|
JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
|
|
365
|
-
WHERE j."${source.localKey}" =
|
|
366
|
-
|
|
367
|
-
);
|
|
438
|
+
WHERE j."${source.localKey}" = ?`;
|
|
439
|
+
sql = appendQueryOptions(sql, params, source, "r");
|
|
440
|
+
return adapter.all(sql, params);
|
|
368
441
|
}
|
|
369
442
|
case "belongsTo": {
|
|
370
443
|
const fkVal = entityRow[source.foreignKey];
|
|
371
444
|
if (fkVal == null) return [];
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
445
|
+
const hasOptions = source.filters?.length || source.softDelete || source.orderBy || source.limit;
|
|
446
|
+
if (!hasOptions) {
|
|
447
|
+
const related = adapter.get(
|
|
448
|
+
`SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
|
|
449
|
+
[fkVal]
|
|
450
|
+
);
|
|
451
|
+
return related ? [related] : [];
|
|
452
|
+
}
|
|
453
|
+
const params = [fkVal];
|
|
454
|
+
let sql = `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`;
|
|
455
|
+
sql = appendQueryOptions(sql, params, source);
|
|
456
|
+
const rows = adapter.all(sql, params);
|
|
457
|
+
return rows.length > 0 ? [rows[0]] : [];
|
|
377
458
|
}
|
|
378
459
|
case "custom":
|
|
379
460
|
return source.query(entityRow, adapter);
|
|
461
|
+
case "enriched": {
|
|
462
|
+
const enriched = { ...entityRow };
|
|
463
|
+
for (const [key, lookup] of Object.entries(source.include)) {
|
|
464
|
+
const fieldName = `_${key}`;
|
|
465
|
+
if (lookup.type === "custom") {
|
|
466
|
+
enriched[fieldName] = JSON.stringify(lookup.query(entityRow, adapter));
|
|
467
|
+
} else {
|
|
468
|
+
const resolved = resolveEntitySource(lookup, entityRow, entityPk, adapter);
|
|
469
|
+
enriched[fieldName] = JSON.stringify(resolved);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return [enriched];
|
|
473
|
+
}
|
|
380
474
|
}
|
|
381
475
|
}
|
|
382
476
|
function truncateContent(content, budget) {
|
|
@@ -589,7 +683,9 @@ var RenderEngine = class {
|
|
|
589
683
|
(0, import_node_fs4.mkdirSync)(entityDir, { recursive: true });
|
|
590
684
|
const renderedFiles = /* @__PURE__ */ new Map();
|
|
591
685
|
for (const [filename, spec] of Object.entries(def.files)) {
|
|
592
|
-
const
|
|
686
|
+
const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
|
|
687
|
+
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
688
|
+
const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
|
|
593
689
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
594
690
|
const content = truncateContent(spec.render(rows), spec.budget);
|
|
595
691
|
renderedFiles.set(filename, content);
|
|
@@ -1524,6 +1620,39 @@ var Lattice = class {
|
|
|
1524
1620
|
}
|
|
1525
1621
|
};
|
|
1526
1622
|
|
|
1623
|
+
// src/render/markdown.ts
|
|
1624
|
+
function frontmatter(fields) {
|
|
1625
|
+
const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
|
|
1626
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
1627
|
+
lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
|
|
1628
|
+
}
|
|
1629
|
+
return `---
|
|
1630
|
+
${lines.join("\n")}
|
|
1631
|
+
---
|
|
1632
|
+
|
|
1633
|
+
`;
|
|
1634
|
+
}
|
|
1635
|
+
function markdownTable(rows, columns) {
|
|
1636
|
+
if (rows.length === 0 || columns.length === 0) return "";
|
|
1637
|
+
const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
|
|
1638
|
+
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
1639
|
+
const body = rows.map((row) => {
|
|
1640
|
+
const cells = columns.map((col) => {
|
|
1641
|
+
const raw = row[col.key];
|
|
1642
|
+
return col.format ? col.format(raw, row) : String(raw ?? "");
|
|
1643
|
+
});
|
|
1644
|
+
return "| " + cells.join(" | ") + " |";
|
|
1645
|
+
});
|
|
1646
|
+
return [header, separator, ...body].join("\n") + "\n";
|
|
1647
|
+
}
|
|
1648
|
+
function slugify(name) {
|
|
1649
|
+
return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1650
|
+
}
|
|
1651
|
+
function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
|
|
1652
|
+
if (content.length <= maxChars) return content;
|
|
1653
|
+
return content.slice(0, maxChars) + notice;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1527
1656
|
// src/session/parser.ts
|
|
1528
1657
|
var import_node_crypto3 = require("crypto");
|
|
1529
1658
|
function generateWriteEntryId(timestamp, agentName, op, table, target) {
|
|
@@ -1929,15 +2058,19 @@ var READ_ONLY_HEADER = createReadOnlyHeader();
|
|
|
1929
2058
|
READ_ONLY_HEADER,
|
|
1930
2059
|
applyWriteEntry,
|
|
1931
2060
|
createReadOnlyHeader,
|
|
2061
|
+
frontmatter,
|
|
1932
2062
|
generateEntryId,
|
|
1933
2063
|
generateWriteEntryId,
|
|
1934
2064
|
manifestPath,
|
|
2065
|
+
markdownTable,
|
|
1935
2066
|
parseConfigFile,
|
|
1936
2067
|
parseConfigString,
|
|
1937
2068
|
parseMarkdownEntries,
|
|
1938
2069
|
parseSessionMD,
|
|
1939
2070
|
parseSessionWrites,
|
|
1940
2071
|
readManifest,
|
|
2072
|
+
slugify,
|
|
2073
|
+
truncate,
|
|
1941
2074
|
validateEntryId,
|
|
1942
2075
|
writeManifest
|
|
1943
2076
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -41,6 +41,32 @@ interface PreparedStatement {
|
|
|
41
41
|
all(...params: unknown[]): Row[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Optional query refinements shared by `hasMany`, `manyToMany`, and
|
|
46
|
+
* `belongsTo` sources. All fields are additive — omitting them preserves
|
|
47
|
+
* the v0.5 behaviour (bare `SELECT *`).
|
|
48
|
+
*/
|
|
49
|
+
interface SourceQueryOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Additional WHERE clauses applied after the relationship join condition.
|
|
52
|
+
* Uses the existing {@link Filter} type.
|
|
53
|
+
*
|
|
54
|
+
* @example `filters: [{ col: 'status', op: 'eq', val: 'active' }]`
|
|
55
|
+
*/
|
|
56
|
+
filters?: Filter[];
|
|
57
|
+
/**
|
|
58
|
+
* Shorthand for `filters: [{ col: 'deleted_at', op: 'isNull' }]`.
|
|
59
|
+
* When `true`, soft-deleted rows are excluded. If both `softDelete`
|
|
60
|
+
* and an explicit `deleted_at` filter are present, the explicit filter wins.
|
|
61
|
+
*/
|
|
62
|
+
softDelete?: boolean;
|
|
63
|
+
/** Column to ORDER BY. Validated against `[a-zA-Z0-9_]`. */
|
|
64
|
+
orderBy?: string;
|
|
65
|
+
/** Sort direction. Defaults to `'asc'`. */
|
|
66
|
+
orderDir?: 'asc' | 'desc';
|
|
67
|
+
/** Maximum number of rows to return. */
|
|
68
|
+
limit?: number;
|
|
69
|
+
}
|
|
44
70
|
/**
|
|
45
71
|
* Yield the entity row itself as a single-element array.
|
|
46
72
|
* Use for the primary entity file (e.g. `AGENT.md`).
|
|
@@ -57,7 +83,7 @@ interface SelfSource {
|
|
|
57
83
|
* source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id' }
|
|
58
84
|
* ```
|
|
59
85
|
*/
|
|
60
|
-
interface HasManySource {
|
|
86
|
+
interface HasManySource extends SourceQueryOptions {
|
|
61
87
|
type: 'hasMany';
|
|
62
88
|
/** The related table to query */
|
|
63
89
|
table: string;
|
|
@@ -84,7 +110,7 @@ interface HasManySource {
|
|
|
84
110
|
* }
|
|
85
111
|
* ```
|
|
86
112
|
*/
|
|
87
|
-
interface ManyToManySource {
|
|
113
|
+
interface ManyToManySource extends SourceQueryOptions {
|
|
88
114
|
type: 'manyToMany';
|
|
89
115
|
/** The junction / association table */
|
|
90
116
|
junctionTable: string;
|
|
@@ -111,7 +137,7 @@ interface ManyToManySource {
|
|
|
111
137
|
* source: { type: 'belongsTo', table: 'teams', foreignKey: 'team_id' }
|
|
112
138
|
* ```
|
|
113
139
|
*/
|
|
114
|
-
interface BelongsToSource {
|
|
140
|
+
interface BelongsToSource extends SourceQueryOptions {
|
|
115
141
|
type: 'belongsTo';
|
|
116
142
|
/** The related table to look up */
|
|
117
143
|
table: string;
|
|
@@ -141,8 +167,57 @@ interface CustomSource {
|
|
|
141
167
|
type: 'custom';
|
|
142
168
|
query: (row: Row, adapter: StorageAdapter) => Row[];
|
|
143
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Sub-lookup definition for an {@link EnrichedSource}.
|
|
172
|
+
* Each lookup resolves related rows and attaches them to the entity row
|
|
173
|
+
* as a `_key` JSON string field.
|
|
174
|
+
*
|
|
175
|
+
* Declarative lookups reuse the same types as file sources (with all
|
|
176
|
+
* query options). Custom lookups provide full control.
|
|
177
|
+
*/
|
|
178
|
+
type EnrichmentLookup = ({
|
|
179
|
+
as?: string;
|
|
180
|
+
} & Omit<HasManySource, 'type'> & {
|
|
181
|
+
type: 'hasMany';
|
|
182
|
+
}) | ({
|
|
183
|
+
as?: string;
|
|
184
|
+
} & Omit<ManyToManySource, 'type'> & {
|
|
185
|
+
type: 'manyToMany';
|
|
186
|
+
}) | ({
|
|
187
|
+
as?: string;
|
|
188
|
+
} & Omit<BelongsToSource, 'type'> & {
|
|
189
|
+
type: 'belongsTo';
|
|
190
|
+
}) | {
|
|
191
|
+
type: 'custom';
|
|
192
|
+
query: (row: Row, adapter: StorageAdapter) => Row[];
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Start with the entity's own row (like `self`) and attach related data
|
|
196
|
+
* as JSON string fields. Each key in `include` becomes a `_key` field
|
|
197
|
+
* on the returned row, containing `JSON.stringify(resolvedRows)`.
|
|
198
|
+
*
|
|
199
|
+
* Use when a single file needs the entity's own data plus related lists
|
|
200
|
+
* (e.g. an org profile that includes its agents and projects).
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* source: {
|
|
205
|
+
* type: 'enriched',
|
|
206
|
+
* include: {
|
|
207
|
+
* agents: { type: 'hasMany', table: 'agents', foreignKey: 'org_id', softDelete: true },
|
|
208
|
+
* projects: { type: 'hasMany', table: 'projects', foreignKey: 'org_id', softDelete: true },
|
|
209
|
+
* },
|
|
210
|
+
* }
|
|
211
|
+
* // Result: [{ ...entityRow, _agents: '[...]', _projects: '[...]' }]
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
interface EnrichedSource {
|
|
215
|
+
type: 'enriched';
|
|
216
|
+
/** Named lookups whose results are attached as `_key` JSON fields. */
|
|
217
|
+
include: Record<string, EnrichmentLookup>;
|
|
218
|
+
}
|
|
144
219
|
/** Union of all supported source types for {@link EntityFileSpec}. */
|
|
145
|
-
type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsToSource | CustomSource;
|
|
220
|
+
type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsToSource | CustomSource | EnrichedSource;
|
|
146
221
|
/**
|
|
147
222
|
* Specification for a single file generated inside an entity's directory.
|
|
148
223
|
*
|
|
@@ -250,6 +325,17 @@ interface EntityContextDefinition {
|
|
|
250
325
|
* Defaults to `[]`.
|
|
251
326
|
*/
|
|
252
327
|
protectedFiles?: string[];
|
|
328
|
+
/**
|
|
329
|
+
* Default query options merged into every `hasMany`, `manyToMany`, and
|
|
330
|
+
* `belongsTo` source in this context. Per-file source options override
|
|
331
|
+
* these defaults. `custom` and `self` sources are unaffected.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* sourceDefaults: { softDelete: true } // exclude soft-deleted rows everywhere
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
sourceDefaults?: SourceQueryOptions;
|
|
253
339
|
}
|
|
254
340
|
|
|
255
341
|
interface CleanupOptions {
|
|
@@ -866,6 +952,69 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
|
|
|
866
952
|
*/
|
|
867
953
|
declare function parseConfigString(yamlContent: string, configDir: string): ParsedConfig;
|
|
868
954
|
|
|
955
|
+
/**
|
|
956
|
+
* Column definition for {@link markdownTable}.
|
|
957
|
+
*/
|
|
958
|
+
interface MarkdownTableColumn {
|
|
959
|
+
/** Row property to read (e.g. `'name'`, `'status'`). */
|
|
960
|
+
key: string;
|
|
961
|
+
/** Column header text displayed in the table. */
|
|
962
|
+
header: string;
|
|
963
|
+
/**
|
|
964
|
+
* Optional per-cell formatter. Receives the raw cell value and the full
|
|
965
|
+
* row so formatters can derive display values from multiple fields.
|
|
966
|
+
*
|
|
967
|
+
* @example `(val) => String(val ?? '—')`
|
|
968
|
+
* @example `(_, row) => \`[\${row.name}](\${row.slug}/DETAIL.md)\``
|
|
969
|
+
*/
|
|
970
|
+
format?: (val: unknown, row: Row) => string;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Generate a YAML-style frontmatter block.
|
|
974
|
+
* Automatically includes `generated_at` with the current ISO timestamp.
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* ```ts
|
|
978
|
+
* frontmatter({ agent: 'Alice', skill_count: 5 })
|
|
979
|
+
* // "---\ngenerated_at: 2026-03-27T...\nagent: Alice\nskill_count: 5\n---\n\n"
|
|
980
|
+
* ```
|
|
981
|
+
*/
|
|
982
|
+
declare function frontmatter(fields: Record<string, string | number | boolean>): string;
|
|
983
|
+
/**
|
|
984
|
+
* Generate a GitHub-Flavoured Markdown table from rows with explicit column
|
|
985
|
+
* configuration. Returns an empty string when `rows` is empty.
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* ```ts
|
|
989
|
+
* markdownTable(rows, [
|
|
990
|
+
* { key: 'name', header: 'Name' },
|
|
991
|
+
* { key: 'status', header: 'Status', format: (v) => String(v ?? '—') },
|
|
992
|
+
* ])
|
|
993
|
+
* ```
|
|
994
|
+
*/
|
|
995
|
+
declare function markdownTable(rows: Row[], columns: MarkdownTableColumn[]): string;
|
|
996
|
+
/**
|
|
997
|
+
* Generate a URL-safe slug from a display name.
|
|
998
|
+
*
|
|
999
|
+
* - Lowercases, strips diacritics, replaces non-alphanumeric runs with `-`,
|
|
1000
|
+
* and trims leading/trailing hyphens.
|
|
1001
|
+
*
|
|
1002
|
+
* @example `slugify('My Agent Name') // 'my-agent-name'`
|
|
1003
|
+
* @example `slugify('José García') // 'jose-garcia'`
|
|
1004
|
+
*/
|
|
1005
|
+
declare function slugify(name: string): string;
|
|
1006
|
+
/**
|
|
1007
|
+
* Truncate content at a character budget.
|
|
1008
|
+
*
|
|
1009
|
+
* When `content.length > maxChars`, slices to `maxChars` and appends `notice`.
|
|
1010
|
+
* Returns `content` unchanged when the budget is not exceeded.
|
|
1011
|
+
*
|
|
1012
|
+
* @param content - The rendered content to truncate
|
|
1013
|
+
* @param maxChars - Maximum character count
|
|
1014
|
+
* @param notice - Appended after truncation (default: standard budget notice)
|
|
1015
|
+
*/
|
|
1016
|
+
declare function truncate(content: string, maxChars: number, notice?: string): string;
|
|
1017
|
+
|
|
869
1018
|
type SessionWriteOp = 'create' | 'update' | 'delete';
|
|
870
1019
|
interface SessionWriteEntry {
|
|
871
1020
|
id: string;
|
|
@@ -1047,4 +1196,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
|
|
|
1047
1196
|
*/
|
|
1048
1197
|
declare const READ_ONLY_HEADER: string;
|
|
1049
1198
|
|
|
1050
|
-
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 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 Migration, type MultiTableDefinition, 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 StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, generateEntryId, generateWriteEntryId, manifestPath, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, validateEntryId, writeManifest };
|
|
1199
|
+
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 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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,32 @@ interface PreparedStatement {
|
|
|
41
41
|
all(...params: unknown[]): Row[];
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Optional query refinements shared by `hasMany`, `manyToMany`, and
|
|
46
|
+
* `belongsTo` sources. All fields are additive — omitting them preserves
|
|
47
|
+
* the v0.5 behaviour (bare `SELECT *`).
|
|
48
|
+
*/
|
|
49
|
+
interface SourceQueryOptions {
|
|
50
|
+
/**
|
|
51
|
+
* Additional WHERE clauses applied after the relationship join condition.
|
|
52
|
+
* Uses the existing {@link Filter} type.
|
|
53
|
+
*
|
|
54
|
+
* @example `filters: [{ col: 'status', op: 'eq', val: 'active' }]`
|
|
55
|
+
*/
|
|
56
|
+
filters?: Filter[];
|
|
57
|
+
/**
|
|
58
|
+
* Shorthand for `filters: [{ col: 'deleted_at', op: 'isNull' }]`.
|
|
59
|
+
* When `true`, soft-deleted rows are excluded. If both `softDelete`
|
|
60
|
+
* and an explicit `deleted_at` filter are present, the explicit filter wins.
|
|
61
|
+
*/
|
|
62
|
+
softDelete?: boolean;
|
|
63
|
+
/** Column to ORDER BY. Validated against `[a-zA-Z0-9_]`. */
|
|
64
|
+
orderBy?: string;
|
|
65
|
+
/** Sort direction. Defaults to `'asc'`. */
|
|
66
|
+
orderDir?: 'asc' | 'desc';
|
|
67
|
+
/** Maximum number of rows to return. */
|
|
68
|
+
limit?: number;
|
|
69
|
+
}
|
|
44
70
|
/**
|
|
45
71
|
* Yield the entity row itself as a single-element array.
|
|
46
72
|
* Use for the primary entity file (e.g. `AGENT.md`).
|
|
@@ -57,7 +83,7 @@ interface SelfSource {
|
|
|
57
83
|
* source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id' }
|
|
58
84
|
* ```
|
|
59
85
|
*/
|
|
60
|
-
interface HasManySource {
|
|
86
|
+
interface HasManySource extends SourceQueryOptions {
|
|
61
87
|
type: 'hasMany';
|
|
62
88
|
/** The related table to query */
|
|
63
89
|
table: string;
|
|
@@ -84,7 +110,7 @@ interface HasManySource {
|
|
|
84
110
|
* }
|
|
85
111
|
* ```
|
|
86
112
|
*/
|
|
87
|
-
interface ManyToManySource {
|
|
113
|
+
interface ManyToManySource extends SourceQueryOptions {
|
|
88
114
|
type: 'manyToMany';
|
|
89
115
|
/** The junction / association table */
|
|
90
116
|
junctionTable: string;
|
|
@@ -111,7 +137,7 @@ interface ManyToManySource {
|
|
|
111
137
|
* source: { type: 'belongsTo', table: 'teams', foreignKey: 'team_id' }
|
|
112
138
|
* ```
|
|
113
139
|
*/
|
|
114
|
-
interface BelongsToSource {
|
|
140
|
+
interface BelongsToSource extends SourceQueryOptions {
|
|
115
141
|
type: 'belongsTo';
|
|
116
142
|
/** The related table to look up */
|
|
117
143
|
table: string;
|
|
@@ -141,8 +167,57 @@ interface CustomSource {
|
|
|
141
167
|
type: 'custom';
|
|
142
168
|
query: (row: Row, adapter: StorageAdapter) => Row[];
|
|
143
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Sub-lookup definition for an {@link EnrichedSource}.
|
|
172
|
+
* Each lookup resolves related rows and attaches them to the entity row
|
|
173
|
+
* as a `_key` JSON string field.
|
|
174
|
+
*
|
|
175
|
+
* Declarative lookups reuse the same types as file sources (with all
|
|
176
|
+
* query options). Custom lookups provide full control.
|
|
177
|
+
*/
|
|
178
|
+
type EnrichmentLookup = ({
|
|
179
|
+
as?: string;
|
|
180
|
+
} & Omit<HasManySource, 'type'> & {
|
|
181
|
+
type: 'hasMany';
|
|
182
|
+
}) | ({
|
|
183
|
+
as?: string;
|
|
184
|
+
} & Omit<ManyToManySource, 'type'> & {
|
|
185
|
+
type: 'manyToMany';
|
|
186
|
+
}) | ({
|
|
187
|
+
as?: string;
|
|
188
|
+
} & Omit<BelongsToSource, 'type'> & {
|
|
189
|
+
type: 'belongsTo';
|
|
190
|
+
}) | {
|
|
191
|
+
type: 'custom';
|
|
192
|
+
query: (row: Row, adapter: StorageAdapter) => Row[];
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Start with the entity's own row (like `self`) and attach related data
|
|
196
|
+
* as JSON string fields. Each key in `include` becomes a `_key` field
|
|
197
|
+
* on the returned row, containing `JSON.stringify(resolvedRows)`.
|
|
198
|
+
*
|
|
199
|
+
* Use when a single file needs the entity's own data plus related lists
|
|
200
|
+
* (e.g. an org profile that includes its agents and projects).
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* source: {
|
|
205
|
+
* type: 'enriched',
|
|
206
|
+
* include: {
|
|
207
|
+
* agents: { type: 'hasMany', table: 'agents', foreignKey: 'org_id', softDelete: true },
|
|
208
|
+
* projects: { type: 'hasMany', table: 'projects', foreignKey: 'org_id', softDelete: true },
|
|
209
|
+
* },
|
|
210
|
+
* }
|
|
211
|
+
* // Result: [{ ...entityRow, _agents: '[...]', _projects: '[...]' }]
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
interface EnrichedSource {
|
|
215
|
+
type: 'enriched';
|
|
216
|
+
/** Named lookups whose results are attached as `_key` JSON fields. */
|
|
217
|
+
include: Record<string, EnrichmentLookup>;
|
|
218
|
+
}
|
|
144
219
|
/** Union of all supported source types for {@link EntityFileSpec}. */
|
|
145
|
-
type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsToSource | CustomSource;
|
|
220
|
+
type EntityFileSource = SelfSource | HasManySource | ManyToManySource | BelongsToSource | CustomSource | EnrichedSource;
|
|
146
221
|
/**
|
|
147
222
|
* Specification for a single file generated inside an entity's directory.
|
|
148
223
|
*
|
|
@@ -250,6 +325,17 @@ interface EntityContextDefinition {
|
|
|
250
325
|
* Defaults to `[]`.
|
|
251
326
|
*/
|
|
252
327
|
protectedFiles?: string[];
|
|
328
|
+
/**
|
|
329
|
+
* Default query options merged into every `hasMany`, `manyToMany`, and
|
|
330
|
+
* `belongsTo` source in this context. Per-file source options override
|
|
331
|
+
* these defaults. `custom` and `self` sources are unaffected.
|
|
332
|
+
*
|
|
333
|
+
* @example
|
|
334
|
+
* ```ts
|
|
335
|
+
* sourceDefaults: { softDelete: true } // exclude soft-deleted rows everywhere
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
sourceDefaults?: SourceQueryOptions;
|
|
253
339
|
}
|
|
254
340
|
|
|
255
341
|
interface CleanupOptions {
|
|
@@ -866,6 +952,69 @@ declare function parseConfigFile(configPath: string): ParsedConfig;
|
|
|
866
952
|
*/
|
|
867
953
|
declare function parseConfigString(yamlContent: string, configDir: string): ParsedConfig;
|
|
868
954
|
|
|
955
|
+
/**
|
|
956
|
+
* Column definition for {@link markdownTable}.
|
|
957
|
+
*/
|
|
958
|
+
interface MarkdownTableColumn {
|
|
959
|
+
/** Row property to read (e.g. `'name'`, `'status'`). */
|
|
960
|
+
key: string;
|
|
961
|
+
/** Column header text displayed in the table. */
|
|
962
|
+
header: string;
|
|
963
|
+
/**
|
|
964
|
+
* Optional per-cell formatter. Receives the raw cell value and the full
|
|
965
|
+
* row so formatters can derive display values from multiple fields.
|
|
966
|
+
*
|
|
967
|
+
* @example `(val) => String(val ?? '—')`
|
|
968
|
+
* @example `(_, row) => \`[\${row.name}](\${row.slug}/DETAIL.md)\``
|
|
969
|
+
*/
|
|
970
|
+
format?: (val: unknown, row: Row) => string;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Generate a YAML-style frontmatter block.
|
|
974
|
+
* Automatically includes `generated_at` with the current ISO timestamp.
|
|
975
|
+
*
|
|
976
|
+
* @example
|
|
977
|
+
* ```ts
|
|
978
|
+
* frontmatter({ agent: 'Alice', skill_count: 5 })
|
|
979
|
+
* // "---\ngenerated_at: 2026-03-27T...\nagent: Alice\nskill_count: 5\n---\n\n"
|
|
980
|
+
* ```
|
|
981
|
+
*/
|
|
982
|
+
declare function frontmatter(fields: Record<string, string | number | boolean>): string;
|
|
983
|
+
/**
|
|
984
|
+
* Generate a GitHub-Flavoured Markdown table from rows with explicit column
|
|
985
|
+
* configuration. Returns an empty string when `rows` is empty.
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* ```ts
|
|
989
|
+
* markdownTable(rows, [
|
|
990
|
+
* { key: 'name', header: 'Name' },
|
|
991
|
+
* { key: 'status', header: 'Status', format: (v) => String(v ?? '—') },
|
|
992
|
+
* ])
|
|
993
|
+
* ```
|
|
994
|
+
*/
|
|
995
|
+
declare function markdownTable(rows: Row[], columns: MarkdownTableColumn[]): string;
|
|
996
|
+
/**
|
|
997
|
+
* Generate a URL-safe slug from a display name.
|
|
998
|
+
*
|
|
999
|
+
* - Lowercases, strips diacritics, replaces non-alphanumeric runs with `-`,
|
|
1000
|
+
* and trims leading/trailing hyphens.
|
|
1001
|
+
*
|
|
1002
|
+
* @example `slugify('My Agent Name') // 'my-agent-name'`
|
|
1003
|
+
* @example `slugify('José García') // 'jose-garcia'`
|
|
1004
|
+
*/
|
|
1005
|
+
declare function slugify(name: string): string;
|
|
1006
|
+
/**
|
|
1007
|
+
* Truncate content at a character budget.
|
|
1008
|
+
*
|
|
1009
|
+
* When `content.length > maxChars`, slices to `maxChars` and appends `notice`.
|
|
1010
|
+
* Returns `content` unchanged when the budget is not exceeded.
|
|
1011
|
+
*
|
|
1012
|
+
* @param content - The rendered content to truncate
|
|
1013
|
+
* @param maxChars - Maximum character count
|
|
1014
|
+
* @param notice - Appended after truncation (default: standard budget notice)
|
|
1015
|
+
*/
|
|
1016
|
+
declare function truncate(content: string, maxChars: number, notice?: string): string;
|
|
1017
|
+
|
|
869
1018
|
type SessionWriteOp = 'create' | 'update' | 'delete';
|
|
870
1019
|
interface SessionWriteEntry {
|
|
871
1020
|
id: string;
|
|
@@ -1047,4 +1196,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
|
|
|
1047
1196
|
*/
|
|
1048
1197
|
declare const READ_ONLY_HEADER: string;
|
|
1049
1198
|
|
|
1050
|
-
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 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 Migration, type MultiTableDefinition, 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 StopFn, type SyncResult, type TableDefinition, type TemplateRenderSpec, type WatchOptions, type WritebackDefinition, applyWriteEntry, createReadOnlyHeader, generateEntryId, generateWriteEntryId, manifestPath, parseConfigFile, parseConfigString, parseMarkdownEntries, parseSessionMD, parseSessionWrites, readManifest, validateEntryId, writeManifest };
|
|
1199
|
+
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 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 };
|
package/dist/index.js
CHANGED
|
@@ -292,6 +292,75 @@ import { join as join4 } from "path";
|
|
|
292
292
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
293
293
|
|
|
294
294
|
// src/render/entity-query.ts
|
|
295
|
+
var SAFE_COL_RE = /^[a-zA-Z0-9_]+$/;
|
|
296
|
+
function effectiveFilters(opts) {
|
|
297
|
+
const filters = opts.filters ? [...opts.filters] : [];
|
|
298
|
+
if (opts.softDelete && !filters.some((f) => f.col === "deleted_at")) {
|
|
299
|
+
filters.unshift({ col: "deleted_at", op: "isNull" });
|
|
300
|
+
}
|
|
301
|
+
return filters;
|
|
302
|
+
}
|
|
303
|
+
function appendQueryOptions(baseSql, params, opts, tableAlias) {
|
|
304
|
+
let sql = baseSql;
|
|
305
|
+
const prefix = tableAlias ? `${tableAlias}.` : "";
|
|
306
|
+
for (const f of effectiveFilters(opts)) {
|
|
307
|
+
if (!SAFE_COL_RE.test(f.col)) continue;
|
|
308
|
+
switch (f.op) {
|
|
309
|
+
case "eq":
|
|
310
|
+
sql += ` AND ${prefix}"${f.col}" = ?`;
|
|
311
|
+
params.push(f.val);
|
|
312
|
+
break;
|
|
313
|
+
case "ne":
|
|
314
|
+
sql += ` AND ${prefix}"${f.col}" != ?`;
|
|
315
|
+
params.push(f.val);
|
|
316
|
+
break;
|
|
317
|
+
case "gt":
|
|
318
|
+
sql += ` AND ${prefix}"${f.col}" > ?`;
|
|
319
|
+
params.push(f.val);
|
|
320
|
+
break;
|
|
321
|
+
case "gte":
|
|
322
|
+
sql += ` AND ${prefix}"${f.col}" >= ?`;
|
|
323
|
+
params.push(f.val);
|
|
324
|
+
break;
|
|
325
|
+
case "lt":
|
|
326
|
+
sql += ` AND ${prefix}"${f.col}" < ?`;
|
|
327
|
+
params.push(f.val);
|
|
328
|
+
break;
|
|
329
|
+
case "lte":
|
|
330
|
+
sql += ` AND ${prefix}"${f.col}" <= ?`;
|
|
331
|
+
params.push(f.val);
|
|
332
|
+
break;
|
|
333
|
+
case "like":
|
|
334
|
+
sql += ` AND ${prefix}"${f.col}" LIKE ?`;
|
|
335
|
+
params.push(f.val);
|
|
336
|
+
break;
|
|
337
|
+
case "in": {
|
|
338
|
+
const arr = f.val;
|
|
339
|
+
if (arr.length === 0) {
|
|
340
|
+
sql += " AND 0";
|
|
341
|
+
} else {
|
|
342
|
+
sql += ` AND ${prefix}"${f.col}" IN (${arr.map(() => "?").join(", ")})`;
|
|
343
|
+
params.push(...arr);
|
|
344
|
+
}
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
case "isNull":
|
|
348
|
+
sql += ` AND ${prefix}"${f.col}" IS NULL`;
|
|
349
|
+
break;
|
|
350
|
+
case "isNotNull":
|
|
351
|
+
sql += ` AND ${prefix}"${f.col}" IS NOT NULL`;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (opts.orderBy && SAFE_COL_RE.test(opts.orderBy)) {
|
|
356
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
357
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
358
|
+
}
|
|
359
|
+
if (opts.limit !== void 0 && opts.limit > 0) {
|
|
360
|
+
sql += ` LIMIT ${Math.floor(opts.limit)}`;
|
|
361
|
+
}
|
|
362
|
+
return sql;
|
|
363
|
+
}
|
|
295
364
|
function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
296
365
|
switch (source.type) {
|
|
297
366
|
case "self":
|
|
@@ -299,32 +368,53 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
|
299
368
|
case "hasMany": {
|
|
300
369
|
const ref = source.references ?? entityPk;
|
|
301
370
|
const pkVal = entityRow[ref];
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
);
|
|
371
|
+
const params = [pkVal];
|
|
372
|
+
let sql = `SELECT * FROM "${source.table}" WHERE "${source.foreignKey}" = ?`;
|
|
373
|
+
sql = appendQueryOptions(sql, params, source);
|
|
374
|
+
return adapter.all(sql, params);
|
|
306
375
|
}
|
|
307
376
|
case "manyToMany": {
|
|
308
377
|
const pkVal = entityRow[entityPk];
|
|
309
378
|
const remotePk = source.references ?? "id";
|
|
310
|
-
|
|
311
|
-
|
|
379
|
+
const params = [pkVal];
|
|
380
|
+
let sql = `SELECT r.* FROM "${source.remoteTable}" r
|
|
312
381
|
JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
|
|
313
|
-
WHERE j."${source.localKey}" =
|
|
314
|
-
|
|
315
|
-
);
|
|
382
|
+
WHERE j."${source.localKey}" = ?`;
|
|
383
|
+
sql = appendQueryOptions(sql, params, source, "r");
|
|
384
|
+
return adapter.all(sql, params);
|
|
316
385
|
}
|
|
317
386
|
case "belongsTo": {
|
|
318
387
|
const fkVal = entityRow[source.foreignKey];
|
|
319
388
|
if (fkVal == null) return [];
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
389
|
+
const hasOptions = source.filters?.length || source.softDelete || source.orderBy || source.limit;
|
|
390
|
+
if (!hasOptions) {
|
|
391
|
+
const related = adapter.get(
|
|
392
|
+
`SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`,
|
|
393
|
+
[fkVal]
|
|
394
|
+
);
|
|
395
|
+
return related ? [related] : [];
|
|
396
|
+
}
|
|
397
|
+
const params = [fkVal];
|
|
398
|
+
let sql = `SELECT * FROM "${source.table}" WHERE "${source.references ?? "id"}" = ?`;
|
|
399
|
+
sql = appendQueryOptions(sql, params, source);
|
|
400
|
+
const rows = adapter.all(sql, params);
|
|
401
|
+
return rows.length > 0 ? [rows[0]] : [];
|
|
325
402
|
}
|
|
326
403
|
case "custom":
|
|
327
404
|
return source.query(entityRow, adapter);
|
|
405
|
+
case "enriched": {
|
|
406
|
+
const enriched = { ...entityRow };
|
|
407
|
+
for (const [key, lookup] of Object.entries(source.include)) {
|
|
408
|
+
const fieldName = `_${key}`;
|
|
409
|
+
if (lookup.type === "custom") {
|
|
410
|
+
enriched[fieldName] = JSON.stringify(lookup.query(entityRow, adapter));
|
|
411
|
+
} else {
|
|
412
|
+
const resolved = resolveEntitySource(lookup, entityRow, entityPk, adapter);
|
|
413
|
+
enriched[fieldName] = JSON.stringify(resolved);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return [enriched];
|
|
417
|
+
}
|
|
328
418
|
}
|
|
329
419
|
}
|
|
330
420
|
function truncateContent(content, budget) {
|
|
@@ -537,7 +627,9 @@ var RenderEngine = class {
|
|
|
537
627
|
mkdirSync2(entityDir, { recursive: true });
|
|
538
628
|
const renderedFiles = /* @__PURE__ */ new Map();
|
|
539
629
|
for (const [filename, spec] of Object.entries(def.files)) {
|
|
540
|
-
const
|
|
630
|
+
const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
|
|
631
|
+
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
632
|
+
const rows = resolveEntitySource(source, entityRow, entityPk, this._adapter);
|
|
541
633
|
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
542
634
|
const content = truncateContent(spec.render(rows), spec.budget);
|
|
543
635
|
renderedFiles.set(filename, content);
|
|
@@ -1472,6 +1564,39 @@ var Lattice = class {
|
|
|
1472
1564
|
}
|
|
1473
1565
|
};
|
|
1474
1566
|
|
|
1567
|
+
// src/render/markdown.ts
|
|
1568
|
+
function frontmatter(fields) {
|
|
1569
|
+
const lines = [`generated_at: "${(/* @__PURE__ */ new Date()).toISOString()}"`];
|
|
1570
|
+
for (const [key, val] of Object.entries(fields)) {
|
|
1571
|
+
lines.push(typeof val === "string" ? `${key}: "${val}"` : `${key}: ${String(val)}`);
|
|
1572
|
+
}
|
|
1573
|
+
return `---
|
|
1574
|
+
${lines.join("\n")}
|
|
1575
|
+
---
|
|
1576
|
+
|
|
1577
|
+
`;
|
|
1578
|
+
}
|
|
1579
|
+
function markdownTable(rows, columns) {
|
|
1580
|
+
if (rows.length === 0 || columns.length === 0) return "";
|
|
1581
|
+
const header = "| " + columns.map((c) => c.header).join(" | ") + " |";
|
|
1582
|
+
const separator = "| " + columns.map(() => "---").join(" | ") + " |";
|
|
1583
|
+
const body = rows.map((row) => {
|
|
1584
|
+
const cells = columns.map((col) => {
|
|
1585
|
+
const raw = row[col.key];
|
|
1586
|
+
return col.format ? col.format(raw, row) : String(raw ?? "");
|
|
1587
|
+
});
|
|
1588
|
+
return "| " + cells.join(" | ") + " |";
|
|
1589
|
+
});
|
|
1590
|
+
return [header, separator, ...body].join("\n") + "\n";
|
|
1591
|
+
}
|
|
1592
|
+
function slugify(name) {
|
|
1593
|
+
return name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\u0131/g, "i").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
|
|
1594
|
+
}
|
|
1595
|
+
function truncate(content, maxChars, notice = "\n\n*[truncated \u2014 context budget exceeded]*") {
|
|
1596
|
+
if (content.length <= maxChars) return content;
|
|
1597
|
+
return content.slice(0, maxChars) + notice;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1475
1600
|
// src/session/parser.ts
|
|
1476
1601
|
import { createHash as createHash2 } from "crypto";
|
|
1477
1602
|
function generateWriteEntryId(timestamp, agentName, op, table, target) {
|
|
@@ -1876,15 +2001,19 @@ export {
|
|
|
1876
2001
|
READ_ONLY_HEADER,
|
|
1877
2002
|
applyWriteEntry,
|
|
1878
2003
|
createReadOnlyHeader,
|
|
2004
|
+
frontmatter,
|
|
1879
2005
|
generateEntryId,
|
|
1880
2006
|
generateWriteEntryId,
|
|
1881
2007
|
manifestPath,
|
|
2008
|
+
markdownTable,
|
|
1882
2009
|
parseConfigFile,
|
|
1883
2010
|
parseConfigString,
|
|
1884
2011
|
parseMarkdownEntries,
|
|
1885
2012
|
parseSessionMD,
|
|
1886
2013
|
parseSessionWrites,
|
|
1887
2014
|
readManifest,
|
|
2015
|
+
slugify,
|
|
2016
|
+
truncate,
|
|
1888
2017
|
validateEntryId,
|
|
1889
2018
|
writeManifest
|
|
1890
2019
|
};
|