latticesql 0.7.0 → 0.8.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/README.md +214 -5
- package/dist/cli.js +25 -4
- package/dist/index.cjs +25 -4
- package/dist/index.d.cts +38 -4
- package/dist/index.d.ts +38 -4
- package/dist/index.js +25 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -356,13 +356,16 @@ db.defineMulti('agent-context', {
|
|
|
356
356
|
db.defineEntityContext(table: string, def: EntityContextDefinition): this
|
|
357
357
|
```
|
|
358
358
|
|
|
359
|
-
Generate a **parallel file-system tree** for an entity type — one subdirectory per row, one file per declared relationship, and an optional combined context file.
|
|
359
|
+
Generate a **parallel file-system tree** for an entity type — one subdirectory per row, one file per declared relationship, and an optional combined context file. Can be called before or after `init()`.
|
|
360
360
|
|
|
361
361
|
```typescript
|
|
362
362
|
db.defineEntityContext('agents', {
|
|
363
363
|
// Derive the subdirectory name for each entity
|
|
364
364
|
slug: (row) => row.slug as string,
|
|
365
365
|
|
|
366
|
+
// Default query options for all relationship sources (v0.6+)
|
|
367
|
+
sourceDefaults: { softDelete: true },
|
|
368
|
+
|
|
366
369
|
// Global index file listing all entities
|
|
367
370
|
index: {
|
|
368
371
|
outputFile: 'agents/AGENTS.md',
|
|
@@ -376,7 +379,8 @@ db.defineEntityContext('agents', {
|
|
|
376
379
|
render: ([r]) => `# ${r.name as string}\n\n${r.bio as string ?? ''}`,
|
|
377
380
|
},
|
|
378
381
|
'TASKS.md': {
|
|
379
|
-
source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id'
|
|
382
|
+
source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id',
|
|
383
|
+
orderBy: 'created_at', orderDir: 'desc', limit: 20 },
|
|
380
384
|
render: (rows) => rows.map((r) => `- ${r.title as string}`).join('\n'),
|
|
381
385
|
omitIfEmpty: true, // skip if no tasks
|
|
382
386
|
budget: 4000, // truncate at 4 000 chars
|
|
@@ -388,6 +392,7 @@ db.defineEntityContext('agents', {
|
|
|
388
392
|
localKey: 'agent_id',
|
|
389
393
|
remoteKey: 'skill_id',
|
|
390
394
|
remoteTable: 'skills',
|
|
395
|
+
orderBy: 'name', // softDelete inherited from sourceDefaults
|
|
391
396
|
},
|
|
392
397
|
render: (rows) => rows.map((r) => `- ${r.name as string}`).join('\n'),
|
|
393
398
|
omitIfEmpty: true,
|
|
@@ -423,11 +428,82 @@ context/
|
|
|
423
428
|
| Type | What it queries |
|
|
424
429
|
|---|---|
|
|
425
430
|
| `{ type: 'self' }` | The entity row itself |
|
|
426
|
-
| `{ type: 'hasMany', table, foreignKey,
|
|
427
|
-
| `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable,
|
|
428
|
-
| `{ type: 'belongsTo', table, foreignKey,
|
|
431
|
+
| `{ type: 'hasMany', table, foreignKey, ... }` | Rows in `table` where `foreignKey = entityPk` |
|
|
432
|
+
| `{ type: 'manyToMany', junctionTable, localKey, remoteKey, remoteTable, ... }` | Remote rows via a junction table |
|
|
433
|
+
| `{ type: 'belongsTo', table, foreignKey, ... }` | Single parent row via FK on this entity (`null` FK → empty) |
|
|
434
|
+
| `{ type: 'enriched', include: { ... } }` | Entity row + related data attached as `_key` JSON fields (v0.7+) |
|
|
429
435
|
| `{ type: 'custom', query: (row, adapter) => Row[] }` | Fully custom synchronous query |
|
|
430
436
|
|
|
437
|
+
#### Source query options (v0.6+)
|
|
438
|
+
|
|
439
|
+
`hasMany`, `manyToMany`, and `belongsTo` sources accept optional query refinements:
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
{
|
|
443
|
+
type: 'hasMany',
|
|
444
|
+
table: 'tasks',
|
|
445
|
+
foreignKey: 'agent_id',
|
|
446
|
+
// Query options (all optional):
|
|
447
|
+
softDelete: true, // exclude rows where deleted_at IS NULL
|
|
448
|
+
filters: [ // additional WHERE clauses (uses existing Filter type)
|
|
449
|
+
{ col: 'status', op: 'eq', val: 'active' },
|
|
450
|
+
],
|
|
451
|
+
orderBy: 'created_at', // ORDER BY column
|
|
452
|
+
orderDir: 'desc', // 'asc' (default) or 'desc'
|
|
453
|
+
limit: 20, // LIMIT N
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The `softDelete: true` shorthand is equivalent to `filters: [{ col: 'deleted_at', op: 'isNull' }]`.
|
|
458
|
+
|
|
459
|
+
#### sourceDefaults (v0.6+)
|
|
460
|
+
|
|
461
|
+
Set default query options for all relationship sources in an entity context:
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
db.defineEntityContext('agents', {
|
|
465
|
+
slug: (row) => row.slug as string,
|
|
466
|
+
sourceDefaults: { softDelete: true }, // applied to all hasMany/manyToMany/belongsTo
|
|
467
|
+
files: {
|
|
468
|
+
'TASKS.md': {
|
|
469
|
+
// softDelete: true is inherited from sourceDefaults
|
|
470
|
+
source: { type: 'hasMany', table: 'tasks', foreignKey: 'agent_id', orderBy: 'created_at' },
|
|
471
|
+
render: (rows) => rows.map((r) => `- ${r.title as string}`).join('\n'),
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Per-file source options override defaults. `custom`, `self`, and `enriched` sources are unaffected.
|
|
478
|
+
|
|
479
|
+
#### Enriched source (v0.7+)
|
|
480
|
+
|
|
481
|
+
Starts with the entity's own row and attaches related data as JSON string fields. Each key in `include` becomes a `_key` field containing `JSON.stringify(resolvedRows)`.
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
'PROFILE.md': {
|
|
485
|
+
source: {
|
|
486
|
+
type: 'enriched',
|
|
487
|
+
include: {
|
|
488
|
+
// Declarative sub-lookups (support all query options)
|
|
489
|
+
skills: { type: 'manyToMany', junctionTable: 'agent_skills',
|
|
490
|
+
localKey: 'agent_id', remoteKey: 'skill_id',
|
|
491
|
+
remoteTable: 'skills', softDelete: true },
|
|
492
|
+
projects: { type: 'hasMany', table: 'projects', foreignKey: 'org_id',
|
|
493
|
+
softDelete: true, orderBy: 'name' },
|
|
494
|
+
// Custom sub-lookup for complex queries
|
|
495
|
+
stats: { type: 'custom', query: (row, adapter) =>
|
|
496
|
+
adapter.all('SELECT COUNT(*) as cnt FROM events WHERE actor_id = ?', [row.id]) },
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
render: ([row]) => {
|
|
500
|
+
const skills = JSON.parse(row._skills as string);
|
|
501
|
+
const projects = JSON.parse(row._projects as string);
|
|
502
|
+
return `# ${row.name}\n\nSkills: ${skills.length}\nProjects: ${projects.length}`;
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
431
507
|
See [docs/entity-context.md](./docs/entity-context.md) for the complete guide.
|
|
432
508
|
|
|
433
509
|
---
|
|
@@ -924,6 +1000,71 @@ db.define('tickets', {
|
|
|
924
1000
|
|
|
925
1001
|
---
|
|
926
1002
|
|
|
1003
|
+
## Markdown utilities (v0.6+)
|
|
1004
|
+
|
|
1005
|
+
Composable helper functions for building render functions. Use inside `render: (rows) => ...` callbacks to reduce boilerplate.
|
|
1006
|
+
|
|
1007
|
+
### `frontmatter(fields)`
|
|
1008
|
+
|
|
1009
|
+
Generate a YAML-style frontmatter block. Automatically includes `generated_at` with the current ISO timestamp.
|
|
1010
|
+
|
|
1011
|
+
```typescript
|
|
1012
|
+
import { frontmatter } from 'latticesql';
|
|
1013
|
+
|
|
1014
|
+
const header = frontmatter({ agent: 'Alice', skill_count: 5 });
|
|
1015
|
+
// ---
|
|
1016
|
+
// generated_at: "2026-03-27T..."
|
|
1017
|
+
// agent: "Alice"
|
|
1018
|
+
// skill_count: 5
|
|
1019
|
+
// ---
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
### `markdownTable(rows, columns)`
|
|
1023
|
+
|
|
1024
|
+
Generate a GitHub-Flavoured Markdown table from rows with explicit column configuration and optional per-cell formatters.
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
import { markdownTable } from 'latticesql';
|
|
1028
|
+
|
|
1029
|
+
const md = markdownTable(rows, [
|
|
1030
|
+
{ key: 'name', header: 'Name' },
|
|
1031
|
+
{ key: 'status', header: 'Status', format: (v) => String(v || '—') },
|
|
1032
|
+
{ key: 'name', header: 'Detail', format: (v, row) => `[view](${row.slug}/DETAIL.md)` },
|
|
1033
|
+
]);
|
|
1034
|
+
// | Name | Status | Detail |
|
|
1035
|
+
// | --- | --- | --- |
|
|
1036
|
+
// | Alice | active | [view](alice/DETAIL.md) |
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
Returns empty string for zero rows. The `format` callback receives `(cellValue, fullRow)`.
|
|
1040
|
+
|
|
1041
|
+
### `slugify(name)`
|
|
1042
|
+
|
|
1043
|
+
Generate a URL-safe slug from a display name — lowercases, strips diacritics, replaces non-alphanumeric runs with hyphens.
|
|
1044
|
+
|
|
1045
|
+
```typescript
|
|
1046
|
+
import { slugify } from 'latticesql';
|
|
1047
|
+
|
|
1048
|
+
slugify('My Agent Name'); // 'my-agent-name'
|
|
1049
|
+
slugify('Jose Garcia'); // 'jose-garcia'
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
### `truncate(content, maxChars, notice?)`
|
|
1053
|
+
|
|
1054
|
+
Truncate content at a character budget. Appends a notice when truncation occurs.
|
|
1055
|
+
|
|
1056
|
+
```typescript
|
|
1057
|
+
import { truncate } from 'latticesql';
|
|
1058
|
+
|
|
1059
|
+
const md = truncate(longContent, 4000);
|
|
1060
|
+
// Appends: "\n\n*[truncated — context budget exceeded]*"
|
|
1061
|
+
|
|
1062
|
+
const md2 = truncate(longContent, 4000, '\n\n[...truncated]');
|
|
1063
|
+
// Custom notice
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
---
|
|
1067
|
+
|
|
927
1068
|
## Entity context directories (v0.5+)
|
|
928
1069
|
|
|
929
1070
|
`defineEntityContext()` is the high-level API for per-entity file generation — the pattern where each entity type gets its own directory tree, with a separate file for each relationship type.
|
|
@@ -1071,6 +1212,74 @@ interface SessionWriteEntry {
|
|
|
1071
1212
|
|
|
1072
1213
|
The processor is responsible for applying the parsed entries to your DB and validating field names against your schema. The `parseSessionWrites` function is pure — no DB access, no side effects.
|
|
1073
1214
|
|
|
1215
|
+
### Full session parser (v0.5.2+)
|
|
1216
|
+
|
|
1217
|
+
For parsing **all** entry types (not just writes), use `parseSessionMD`:
|
|
1218
|
+
|
|
1219
|
+
```ts
|
|
1220
|
+
import { parseSessionMD, parseMarkdownEntries } from 'latticesql';
|
|
1221
|
+
|
|
1222
|
+
// Parse YAML-delimited entries (--- header --- body ===)
|
|
1223
|
+
const result = parseSessionMD(content, startOffset);
|
|
1224
|
+
// result.entries: SessionEntry[] — all types: event, learning, status, write, etc.
|
|
1225
|
+
// result.errors: ParseError[]
|
|
1226
|
+
// result.lastOffset: number — for incremental parsing
|
|
1227
|
+
|
|
1228
|
+
// Parse markdown heading entries (## timestamp — description)
|
|
1229
|
+
const mdResult = parseMarkdownEntries(content, 'agent-name', startOffset);
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
#### Configurable entry types (v0.5.5+)
|
|
1233
|
+
|
|
1234
|
+
By default, the parser validates against a built-in set of entry types. Override via `SessionParseOptions`:
|
|
1235
|
+
|
|
1236
|
+
```ts
|
|
1237
|
+
import { parseSessionMD, DEFAULT_ENTRY_TYPES, DEFAULT_TYPE_ALIASES } from 'latticesql';
|
|
1238
|
+
|
|
1239
|
+
// Accept any type (no validation)
|
|
1240
|
+
parseSessionMD(content, 0, { validTypes: null });
|
|
1241
|
+
|
|
1242
|
+
// Custom type set
|
|
1243
|
+
parseSessionMD(content, 0, {
|
|
1244
|
+
validTypes: new Set(['alert', 'todo', 'write']),
|
|
1245
|
+
typeAliases: { warning: 'alert', task: 'todo' },
|
|
1246
|
+
});
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
### Read-only header (v0.5.5+)
|
|
1250
|
+
|
|
1251
|
+
All generated context files should carry a read-only header. Use the default or create a custom one:
|
|
1252
|
+
|
|
1253
|
+
```ts
|
|
1254
|
+
import { READ_ONLY_HEADER, createReadOnlyHeader } from 'latticesql';
|
|
1255
|
+
|
|
1256
|
+
// Default: "generated by Lattice"
|
|
1257
|
+
const header = READ_ONLY_HEADER;
|
|
1258
|
+
|
|
1259
|
+
// Custom generator name and docs reference
|
|
1260
|
+
const custom = createReadOnlyHeader({
|
|
1261
|
+
generator: 'my-sync-tool',
|
|
1262
|
+
docsRef: 'https://example.com/docs/sessions',
|
|
1263
|
+
});
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
### Write applicator (v0.5.2+)
|
|
1267
|
+
|
|
1268
|
+
Apply parsed write entries to a better-sqlite3 database with schema validation:
|
|
1269
|
+
|
|
1270
|
+
```ts
|
|
1271
|
+
import { applyWriteEntry } from 'latticesql';
|
|
1272
|
+
|
|
1273
|
+
const result = applyWriteEntry(db, writeEntry);
|
|
1274
|
+
if (result.ok) {
|
|
1275
|
+
console.log(`Applied to ${result.table}, record ${result.recordId}`);
|
|
1276
|
+
} else {
|
|
1277
|
+
console.error(result.reason);
|
|
1278
|
+
}
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
Validates table existence, field names against schema, and uses soft-delete when a `deleted_at` column exists.
|
|
1282
|
+
|
|
1074
1283
|
---
|
|
1075
1284
|
|
|
1076
1285
|
## YAML config (v0.4+)
|
package/dist/cli.js
CHANGED
|
@@ -666,9 +666,18 @@ function appendQueryOptions(baseSql, params, opts, tableAlias) {
|
|
|
666
666
|
break;
|
|
667
667
|
}
|
|
668
668
|
}
|
|
669
|
-
if (opts.orderBy
|
|
670
|
-
|
|
671
|
-
|
|
669
|
+
if (opts.orderBy) {
|
|
670
|
+
if (typeof opts.orderBy === "string") {
|
|
671
|
+
if (SAFE_COL_RE.test(opts.orderBy)) {
|
|
672
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
673
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
const clauses = opts.orderBy.filter((spec) => SAFE_COL_RE.test(spec.col)).map((spec) => `${prefix}"${spec.col}" ${spec.dir === "desc" ? "DESC" : "ASC"}`);
|
|
677
|
+
if (clauses.length > 0) {
|
|
678
|
+
sql += ` ORDER BY ${clauses.join(", ")}`;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
672
681
|
}
|
|
673
682
|
if (opts.limit !== void 0 && opts.limit > 0) {
|
|
674
683
|
sql += ` LIMIT ${Math.floor(opts.limit)}`;
|
|
@@ -691,7 +700,19 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
|
691
700
|
const pkVal = entityRow[entityPk];
|
|
692
701
|
const remotePk = source.references ?? "id";
|
|
693
702
|
const params = [pkVal];
|
|
694
|
-
let
|
|
703
|
+
let selectCols = "r.*";
|
|
704
|
+
if (source.junctionColumns?.length) {
|
|
705
|
+
const jCols = source.junctionColumns.map((jc) => {
|
|
706
|
+
if (typeof jc === "string") {
|
|
707
|
+
if (!SAFE_COL_RE.test(jc)) return null;
|
|
708
|
+
return `j."${jc}"`;
|
|
709
|
+
}
|
|
710
|
+
if (!SAFE_COL_RE.test(jc.col) || !SAFE_COL_RE.test(jc.as)) return null;
|
|
711
|
+
return `j."${jc.col}" AS "${jc.as}"`;
|
|
712
|
+
}).filter(Boolean);
|
|
713
|
+
if (jCols.length > 0) selectCols += ", " + jCols.join(", ");
|
|
714
|
+
}
|
|
715
|
+
let sql = `SELECT ${selectCols} FROM "${source.remoteTable}" r
|
|
695
716
|
JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
|
|
696
717
|
WHERE j."${source.localKey}" = ?`;
|
|
697
718
|
sql = appendQueryOptions(sql, params, source, "r");
|
package/dist/index.cjs
CHANGED
|
@@ -408,9 +408,18 @@ function appendQueryOptions(baseSql, params, opts, tableAlias) {
|
|
|
408
408
|
break;
|
|
409
409
|
}
|
|
410
410
|
}
|
|
411
|
-
if (opts.orderBy
|
|
412
|
-
|
|
413
|
-
|
|
411
|
+
if (opts.orderBy) {
|
|
412
|
+
if (typeof opts.orderBy === "string") {
|
|
413
|
+
if (SAFE_COL_RE.test(opts.orderBy)) {
|
|
414
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
415
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
const clauses = opts.orderBy.filter((spec) => SAFE_COL_RE.test(spec.col)).map((spec) => `${prefix}"${spec.col}" ${spec.dir === "desc" ? "DESC" : "ASC"}`);
|
|
419
|
+
if (clauses.length > 0) {
|
|
420
|
+
sql += ` ORDER BY ${clauses.join(", ")}`;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
414
423
|
}
|
|
415
424
|
if (opts.limit !== void 0 && opts.limit > 0) {
|
|
416
425
|
sql += ` LIMIT ${Math.floor(opts.limit)}`;
|
|
@@ -433,7 +442,19 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
|
433
442
|
const pkVal = entityRow[entityPk];
|
|
434
443
|
const remotePk = source.references ?? "id";
|
|
435
444
|
const params = [pkVal];
|
|
436
|
-
let
|
|
445
|
+
let selectCols = "r.*";
|
|
446
|
+
if (source.junctionColumns?.length) {
|
|
447
|
+
const jCols = source.junctionColumns.map((jc) => {
|
|
448
|
+
if (typeof jc === "string") {
|
|
449
|
+
if (!SAFE_COL_RE.test(jc)) return null;
|
|
450
|
+
return `j."${jc}"`;
|
|
451
|
+
}
|
|
452
|
+
if (!SAFE_COL_RE.test(jc.col) || !SAFE_COL_RE.test(jc.as)) return null;
|
|
453
|
+
return `j."${jc.col}" AS "${jc.as}"`;
|
|
454
|
+
}).filter(Boolean);
|
|
455
|
+
if (jCols.length > 0) selectCols += ", " + jCols.join(", ");
|
|
456
|
+
}
|
|
457
|
+
let sql = `SELECT ${selectCols} FROM "${source.remoteTable}" r
|
|
437
458
|
JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
|
|
438
459
|
WHERE j."${source.localKey}" = ?`;
|
|
439
460
|
sql = appendQueryOptions(sql, params, source, "r");
|
package/dist/index.d.cts
CHANGED
|
@@ -60,13 +60,33 @@ interface SourceQueryOptions {
|
|
|
60
60
|
* and an explicit `deleted_at` filter are present, the explicit filter wins.
|
|
61
61
|
*/
|
|
62
62
|
softDelete?: boolean;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Column(s) to ORDER BY. Validated against `[a-zA-Z0-9_]`.
|
|
65
|
+
* - `string` — single column (use `orderDir` for direction)
|
|
66
|
+
* - `OrderBySpec[]` — multi-column with per-column direction
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* orderBy: 'name' // single column
|
|
71
|
+
* orderBy: [{ col: 'severity' }, { col: 'timestamp', dir: 'desc' }] // multi
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
orderBy?: string | OrderBySpec[];
|
|
75
|
+
/** Sort direction when `orderBy` is a string. Defaults to `'asc'`. */
|
|
66
76
|
orderDir?: 'asc' | 'desc';
|
|
67
77
|
/** Maximum number of rows to return. */
|
|
68
78
|
limit?: number;
|
|
69
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* A single ORDER BY column with optional direction.
|
|
82
|
+
* Used in the array form of `SourceQueryOptions.orderBy`.
|
|
83
|
+
*/
|
|
84
|
+
interface OrderBySpec {
|
|
85
|
+
/** Column name (validated against `[a-zA-Z0-9_]`). */
|
|
86
|
+
col: string;
|
|
87
|
+
/** Sort direction. Defaults to `'asc'`. */
|
|
88
|
+
dir?: 'asc' | 'desc';
|
|
89
|
+
}
|
|
70
90
|
/**
|
|
71
91
|
* Yield the entity row itself as a single-element array.
|
|
72
92
|
* Use for the primary entity file (e.g. `AGENT.md`).
|
|
@@ -125,6 +145,20 @@ interface ManyToManySource extends SourceQueryOptions {
|
|
|
125
145
|
* Defaults to `'id'`.
|
|
126
146
|
*/
|
|
127
147
|
references?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Columns from the junction table to include in each result row.
|
|
150
|
+
* Use a string for the column name as-is, or `{ col, as }` to alias.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* junctionColumns: ['source', { col: 'role', as: 'agent_role' }]
|
|
155
|
+
* // Adds j."source" and j."role" AS "agent_role" to each row
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
junctionColumns?: Array<string | {
|
|
159
|
+
col: string;
|
|
160
|
+
as: string;
|
|
161
|
+
}>;
|
|
128
162
|
}
|
|
129
163
|
/**
|
|
130
164
|
* Query the single row that this entity belongs to via a foreign key on
|
|
@@ -1196,4 +1230,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
|
|
|
1196
1230
|
*/
|
|
1197
1231
|
declare const READ_ONLY_HEADER: string;
|
|
1198
1232
|
|
|
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 };
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -60,13 +60,33 @@ interface SourceQueryOptions {
|
|
|
60
60
|
* and an explicit `deleted_at` filter are present, the explicit filter wins.
|
|
61
61
|
*/
|
|
62
62
|
softDelete?: boolean;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Column(s) to ORDER BY. Validated against `[a-zA-Z0-9_]`.
|
|
65
|
+
* - `string` — single column (use `orderDir` for direction)
|
|
66
|
+
* - `OrderBySpec[]` — multi-column with per-column direction
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* orderBy: 'name' // single column
|
|
71
|
+
* orderBy: [{ col: 'severity' }, { col: 'timestamp', dir: 'desc' }] // multi
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
orderBy?: string | OrderBySpec[];
|
|
75
|
+
/** Sort direction when `orderBy` is a string. Defaults to `'asc'`. */
|
|
66
76
|
orderDir?: 'asc' | 'desc';
|
|
67
77
|
/** Maximum number of rows to return. */
|
|
68
78
|
limit?: number;
|
|
69
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* A single ORDER BY column with optional direction.
|
|
82
|
+
* Used in the array form of `SourceQueryOptions.orderBy`.
|
|
83
|
+
*/
|
|
84
|
+
interface OrderBySpec {
|
|
85
|
+
/** Column name (validated against `[a-zA-Z0-9_]`). */
|
|
86
|
+
col: string;
|
|
87
|
+
/** Sort direction. Defaults to `'asc'`. */
|
|
88
|
+
dir?: 'asc' | 'desc';
|
|
89
|
+
}
|
|
70
90
|
/**
|
|
71
91
|
* Yield the entity row itself as a single-element array.
|
|
72
92
|
* Use for the primary entity file (e.g. `AGENT.md`).
|
|
@@ -125,6 +145,20 @@ interface ManyToManySource extends SourceQueryOptions {
|
|
|
125
145
|
* Defaults to `'id'`.
|
|
126
146
|
*/
|
|
127
147
|
references?: string;
|
|
148
|
+
/**
|
|
149
|
+
* Columns from the junction table to include in each result row.
|
|
150
|
+
* Use a string for the column name as-is, or `{ col, as }` to alias.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* junctionColumns: ['source', { col: 'role', as: 'agent_role' }]
|
|
155
|
+
* // Adds j."source" and j."role" AS "agent_role" to each row
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
junctionColumns?: Array<string | {
|
|
159
|
+
col: string;
|
|
160
|
+
as: string;
|
|
161
|
+
}>;
|
|
128
162
|
}
|
|
129
163
|
/**
|
|
130
164
|
* Query the single row that this entity belongs to via a foreign key on
|
|
@@ -1196,4 +1230,4 @@ declare function createReadOnlyHeader(options?: ReadOnlyHeaderOptions): string;
|
|
|
1196
1230
|
*/
|
|
1197
1231
|
declare const READ_ONLY_HEADER: string;
|
|
1198
1232
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -352,9 +352,18 @@ function appendQueryOptions(baseSql, params, opts, tableAlias) {
|
|
|
352
352
|
break;
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
if (opts.orderBy
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
if (opts.orderBy) {
|
|
356
|
+
if (typeof opts.orderBy === "string") {
|
|
357
|
+
if (SAFE_COL_RE.test(opts.orderBy)) {
|
|
358
|
+
const dir = opts.orderDir === "desc" ? "DESC" : "ASC";
|
|
359
|
+
sql += ` ORDER BY ${prefix}"${opts.orderBy}" ${dir}`;
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
const clauses = opts.orderBy.filter((spec) => SAFE_COL_RE.test(spec.col)).map((spec) => `${prefix}"${spec.col}" ${spec.dir === "desc" ? "DESC" : "ASC"}`);
|
|
363
|
+
if (clauses.length > 0) {
|
|
364
|
+
sql += ` ORDER BY ${clauses.join(", ")}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
358
367
|
}
|
|
359
368
|
if (opts.limit !== void 0 && opts.limit > 0) {
|
|
360
369
|
sql += ` LIMIT ${Math.floor(opts.limit)}`;
|
|
@@ -377,7 +386,19 @@ function resolveEntitySource(source, entityRow, entityPk, adapter) {
|
|
|
377
386
|
const pkVal = entityRow[entityPk];
|
|
378
387
|
const remotePk = source.references ?? "id";
|
|
379
388
|
const params = [pkVal];
|
|
380
|
-
let
|
|
389
|
+
let selectCols = "r.*";
|
|
390
|
+
if (source.junctionColumns?.length) {
|
|
391
|
+
const jCols = source.junctionColumns.map((jc) => {
|
|
392
|
+
if (typeof jc === "string") {
|
|
393
|
+
if (!SAFE_COL_RE.test(jc)) return null;
|
|
394
|
+
return `j."${jc}"`;
|
|
395
|
+
}
|
|
396
|
+
if (!SAFE_COL_RE.test(jc.col) || !SAFE_COL_RE.test(jc.as)) return null;
|
|
397
|
+
return `j."${jc.col}" AS "${jc.as}"`;
|
|
398
|
+
}).filter(Boolean);
|
|
399
|
+
if (jCols.length > 0) selectCols += ", " + jCols.join(", ");
|
|
400
|
+
}
|
|
401
|
+
let sql = `SELECT ${selectCols} FROM "${source.remoteTable}" r
|
|
381
402
|
JOIN "${source.junctionTable}" j ON j."${source.remoteKey}" = r."${remotePk}"
|
|
382
403
|
WHERE j."${source.localKey}" = ?`;
|
|
383
404
|
sql = appendQueryOptions(sql, params, source, "r");
|