latticesql 3.1.0 → 3.2.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 +4 -0
- package/dist/cli.js +1996 -878
- package/dist/index.cjs +352 -187
- package/dist/index.d.cts +15 -20
- package/dist/index.d.ts +15 -20
- package/dist/index.js +352 -187
- package/docs/api-reference.md +1370 -0
- package/docs/architecture.md +331 -0
- package/docs/assistant.md +138 -0
- package/docs/cli.md +515 -0
- package/docs/cloud.md +675 -0
- package/docs/collaboration.md +85 -0
- package/docs/configuration.md +416 -0
- package/docs/entity-context.md +510 -0
- package/docs/examples/agent-system.md +313 -0
- package/docs/examples/cms.md +366 -0
- package/docs/examples/ticket-tracker.md +313 -0
- package/docs/migrations.md +272 -0
- package/docs/templates.md +338 -0
- package/docs/workspaces.md +81 -0
- package/package.json +3 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Multiplayer cloud editing
|
|
2
|
+
|
|
3
|
+
When several people open `lattice gui` against the **same shared cloud
|
|
4
|
+
(Postgres) database**, Lattice 1.16 makes concurrent editing live and
|
|
5
|
+
loss-free. Everything here is **cloud-only** — a local SQLite GUI is a single
|
|
6
|
+
writer and behaves exactly as before.
|
|
7
|
+
|
|
8
|
+
## How it works
|
|
9
|
+
|
|
10
|
+
Each person runs their own local GUI server, all pointed at one cloud Postgres —
|
|
11
|
+
each connecting as their **own scoped, non-superuser Postgres role** (see
|
|
12
|
+
[cloud.md](cloud.md)). Postgres Row-Level Security confines every read and write to
|
|
13
|
+
the rows that role may see, so collaboration is naturally scoped: you only ever see
|
|
14
|
+
and flash on changes to rows shared with you. Two channels carry change:
|
|
15
|
+
|
|
16
|
+
- **`RealtimeBroker`** holds a dedicated `pg.Client` running
|
|
17
|
+
`LISTEN lattice_changes` and forwards every `NOTIFY` to the browser over SSE
|
|
18
|
+
(`GET /api/realtime/stream`). Use a **session-mode** connection (e.g. the
|
|
19
|
+
Supabase pooler on port 5432) — transaction-mode poolers silently drop
|
|
20
|
+
`LISTEN`.
|
|
21
|
+
- The **`__lattice_changes`** table is the append-only change feed: each row carries
|
|
22
|
+
a monotonic `seq`, the `table_name`, the `pk`, the `op` (`upsert`/`delete`), the
|
|
23
|
+
`owner_role`, and `created_at`. The per-table RLS trigger writes one entry per
|
|
24
|
+
insert/update/delete; an `AFTER INSERT` trigger emits the NOTIFY carrying only that
|
|
25
|
+
metadata — **never row content** — so clients re-fetch the affected row _through
|
|
26
|
+
RLS_ (which keeps the payload tiny and never broadcasts another member's data).
|
|
27
|
+
|
|
28
|
+
## What you see
|
|
29
|
+
|
|
30
|
+
- **Live share / un-share** — when a row's owner changes its visibility
|
|
31
|
+
(`private` ↔ `everyone`, via `/api/cloud/share`), the row appears or disappears in
|
|
32
|
+
every other member's view on the next broadcast; no page reload. Sharing is
|
|
33
|
+
per-**row** under RLS, not per-table.
|
|
34
|
+
- **Last edited by** — the row detail shows `Last edited by <role> · <time ago>`,
|
|
35
|
+
resolved from the change feed's `owner_role` + `created_at`
|
|
36
|
+
(`GET /api/tables/:t/last-edited`).
|
|
37
|
+
- **Flash + counts** — a row visible in the current view flashes when another
|
|
38
|
+
editor changes it (honoring `prefers-reduced-motion`); changes to other
|
|
39
|
+
tables bump a per-table unseen-change badge in the sidebar.
|
|
40
|
+
- **Offline editing** — see below.
|
|
41
|
+
|
|
42
|
+
## Offline editing
|
|
43
|
+
|
|
44
|
+
When the cloud is unreachable (the realtime channel is disconnected), row edits
|
|
45
|
+
are persisted to an IndexedDB queue instead of being lost, and a top-bar pill
|
|
46
|
+
shows the pending count. On reconnect, `drainQueue()` replays them **in
|
|
47
|
+
edit-timestamp (`client_ts`) order**, each carrying its `X-Lattice-Edit-Id` and
|
|
48
|
+
`X-Lattice-Client-Ts` headers. The server records `client_ts` on the envelope
|
|
49
|
+
(preserving true edit order) and **no-ops a re-sent `edit_id`**
|
|
50
|
+
(`findEnvelopeByEditId`), so a replay after a flaky reconnect can't double-apply.
|
|
51
|
+
|
|
52
|
+
> Optimistic local re-render of a queued edit _before_ it syncs is a documented
|
|
53
|
+
> follow-on; today a queued edit is captured + toasted ("saved offline") and
|
|
54
|
+
> appears once it replays.
|
|
55
|
+
|
|
56
|
+
## Conflict policy
|
|
57
|
+
|
|
58
|
+
**Row edits — last-write-wins by edit timestamp, every version recoverable.**
|
|
59
|
+
Concurrent edits to the same row are applied in arrival order; the live row
|
|
60
|
+
reflects the last write, and _every_ prior version is retained in the
|
|
61
|
+
`__lattice_changes` feed and readable via
|
|
62
|
+
`GET /api/tables/:table/rows/:id/history` (newest first). Nothing is silently
|
|
63
|
+
destroyed — an overwritten value is always recoverable.
|
|
64
|
+
|
|
65
|
+
**A row that was made private out from under you** simply stops being returned —
|
|
66
|
+
Postgres RLS excludes it, so a subsequent read returns nothing and a write affects
|
|
67
|
+
zero rows. Only the row's owner may change its visibility; the owner always retains
|
|
68
|
+
access to their own rows.
|
|
69
|
+
|
|
70
|
+
**Sharing changes** go through the owner-only `lattice_set_row_visibility` SQL
|
|
71
|
+
function, which updates `__lattice_owners.visibility` for a single row. Because each
|
|
72
|
+
change is one row's visibility flip recorded in the change feed, clients converge on
|
|
73
|
+
the latest state and the history is recoverable.
|
|
74
|
+
|
|
75
|
+
**Data-model edits** use `schema_version` as an optimistic-concurrency token. Each
|
|
76
|
+
table's `schemaVersion` is surfaced on `/api/entities`; a client edit carries its
|
|
77
|
+
base version so a stale edit can be rejected and the client re-fetch + re-issue
|
|
78
|
+
against the current schema.
|
|
79
|
+
|
|
80
|
+
## Ordering & clock skew
|
|
81
|
+
|
|
82
|
+
`seq` is the **only** authoritative ordering key — history queries order by it.
|
|
83
|
+
`client_ts` is for display and to preserve recorded edit order across an offline
|
|
84
|
+
replay; it is **never** used for correctness ordering, so a client with a wrong
|
|
85
|
+
clock can't reorder the canonical log.
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# Configuration Guide
|
|
2
|
+
|
|
3
|
+
Complete reference for `lattice.config.yml` — the YAML schema config format introduced in v0.4.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Overview](#overview)
|
|
10
|
+
- [Top-level structure](#top-level-structure)
|
|
11
|
+
- [Field types](#field-types)
|
|
12
|
+
- [Field options](#field-options)
|
|
13
|
+
- [Relationships (`ref`)](#relationships-ref)
|
|
14
|
+
- [Render specs](#render-specs)
|
|
15
|
+
- [Primary keys](#primary-keys)
|
|
16
|
+
- [Output file paths](#output-file-paths)
|
|
17
|
+
- [Multiple entities](#multiple-entities)
|
|
18
|
+
- [Programmatic config API](#programmatic-config-api)
|
|
19
|
+
- [Complete example](#complete-example)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Overview
|
|
24
|
+
|
|
25
|
+
`lattice.config.yml` is a declarative schema file. It defines:
|
|
26
|
+
|
|
27
|
+
- Where the SQLite database lives
|
|
28
|
+
- What tables exist and what columns they have
|
|
29
|
+
- How each table's rows are rendered into LLM context files
|
|
30
|
+
- Where those context files are written
|
|
31
|
+
|
|
32
|
+
Place it at the root of your project (or anywhere — pass `--config` to override):
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
my-project/
|
|
36
|
+
├── lattice.config.yml
|
|
37
|
+
├── data/
|
|
38
|
+
│ └── app.db
|
|
39
|
+
└── context/
|
|
40
|
+
├── AGENTS.md
|
|
41
|
+
└── TASKS.md
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Top-level structure
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
db: ./data/app.db # Required — path to SQLite file
|
|
50
|
+
entities: # Required — one key per table
|
|
51
|
+
table_name:
|
|
52
|
+
fields: ...
|
|
53
|
+
render: ...
|
|
54
|
+
outputFile: ...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
| Key | Type | Required | Description |
|
|
58
|
+
| ---------- | ------ | -------- | --------------------------------------------------------- |
|
|
59
|
+
| `db` | string | yes | Path to the SQLite database, relative to this config file |
|
|
60
|
+
| `entities` | object | yes | Map of entity (table) name → entity definition |
|
|
61
|
+
|
|
62
|
+
The `db` path is resolved relative to the directory containing `lattice.config.yml`, not `process.cwd()`. Using `:memory:` for `db` is valid for testing.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Field types
|
|
67
|
+
|
|
68
|
+
Each field in an entity's `fields` map must have a `type`. The supported types and their mappings:
|
|
69
|
+
|
|
70
|
+
| YAML type | SQLite column type | TypeScript type |
|
|
71
|
+
| ---------- | ------------------ | --------------- |
|
|
72
|
+
| `uuid` | `TEXT` | `string` |
|
|
73
|
+
| `text` | `TEXT` | `string` |
|
|
74
|
+
| `integer` | `INTEGER` | `number` |
|
|
75
|
+
| `int` | `INTEGER` | `number` |
|
|
76
|
+
| `real` | `REAL` | `number` |
|
|
77
|
+
| `float` | `REAL` | `number` |
|
|
78
|
+
| `boolean` | `INTEGER` | `boolean` |
|
|
79
|
+
| `bool` | `INTEGER` | `boolean` |
|
|
80
|
+
| `datetime` | `TEXT` | `string` |
|
|
81
|
+
| `date` | `TEXT` | `string` |
|
|
82
|
+
| `blob` | `BLOB` | `Buffer` |
|
|
83
|
+
|
|
84
|
+
`uuid` and `text` are both stored as `TEXT` — the distinction is semantic: use `uuid` for ID columns, `text` for everything else. Use `boolean`/`bool` for true/false values stored as SQLite integers.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Field options
|
|
89
|
+
|
|
90
|
+
Every field accepts these options in addition to `type`:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
entities:
|
|
94
|
+
task:
|
|
95
|
+
fields:
|
|
96
|
+
id: { type: uuid, primaryKey: true }
|
|
97
|
+
title: { type: text, required: true }
|
|
98
|
+
status: { type: text, default: open }
|
|
99
|
+
priority: { type: integer, default: 1 }
|
|
100
|
+
assignee_id: { type: uuid, ref: user }
|
|
101
|
+
deleted_at: { type: datetime }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Option | Type | Description |
|
|
105
|
+
| ------------ | ------------------------- | --------------------------------------------------------------------------------- |
|
|
106
|
+
| `type` | `LatticeFieldType` | **Required.** Column data type (see table above) |
|
|
107
|
+
| `primaryKey` | `boolean` | Mark this field as the primary key. Generates `TEXT PRIMARY KEY` (for uuid/text) |
|
|
108
|
+
| `required` | `boolean` | Column is `NOT NULL`. Cannot be used together with `primaryKey` |
|
|
109
|
+
| `default` | string / number / boolean | SQL `DEFAULT` value. Strings are quoted; numbers are unquoted |
|
|
110
|
+
| `ref` | string | Foreign-key reference to another entity (see [Relationships](#relationships-ref)) |
|
|
111
|
+
|
|
112
|
+
### Generated SQL
|
|
113
|
+
|
|
114
|
+
```yaml
|
|
115
|
+
id: { type: uuid, primaryKey: true } → "id" TEXT PRIMARY KEY
|
|
116
|
+
title: { type: text, required: true } → "title" TEXT NOT NULL
|
|
117
|
+
status: { type: text, default: open } → "status" TEXT DEFAULT 'open'
|
|
118
|
+
priority: { type: integer, default: 1 } → "priority" INTEGER DEFAULT 1
|
|
119
|
+
notes: { type: text } → "notes" TEXT
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Relationships (`ref`)
|
|
125
|
+
|
|
126
|
+
Adding `ref: <entity>` to a field automatically creates a `belongsTo` relationship in the compiled `TableDefinition`.
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
entities:
|
|
130
|
+
ticket:
|
|
131
|
+
fields:
|
|
132
|
+
id: { type: uuid, primaryKey: true }
|
|
133
|
+
assignee_id: { type: uuid, ref: user }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This generates:
|
|
137
|
+
|
|
138
|
+
- Column: `"assignee_id" TEXT`
|
|
139
|
+
- Relation: `assignee: { type: 'belongsTo', table: 'user', foreignKey: 'assignee_id' }`
|
|
140
|
+
|
|
141
|
+
**Relation name derivation:** If the field name ends with `_id`, the suffix is stripped to form the relation name. Otherwise the full field name is used:
|
|
142
|
+
|
|
143
|
+
| Field name | Relation name |
|
|
144
|
+
| ------------- | ------------- |
|
|
145
|
+
| `assignee_id` | `assignee` |
|
|
146
|
+
| `project_id` | `project` |
|
|
147
|
+
| `parent_id` | `parent` |
|
|
148
|
+
| `author` | `author` |
|
|
149
|
+
|
|
150
|
+
The relation name is used in `{{relationName.field}}` interpolation strings inside render templates.
|
|
151
|
+
|
|
152
|
+
SQLite does not enforce foreign key constraints by default. Lattice stores `ref` as metadata for template rendering — no `FOREIGN KEY` constraint is added to the SQL schema.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Render specs
|
|
157
|
+
|
|
158
|
+
The `render` key controls how rows are turned into context file content. It accepts three forms:
|
|
159
|
+
|
|
160
|
+
### Form 1 — Built-in template name (string)
|
|
161
|
+
|
|
162
|
+
```yaml
|
|
163
|
+
entities:
|
|
164
|
+
agent:
|
|
165
|
+
render: default-table
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Available built-in templates: `default-list`, `default-table`, `default-detail`, `default-json`.
|
|
169
|
+
|
|
170
|
+
### Form 2 — Template with formatRow hook (object)
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
entities:
|
|
174
|
+
ticket:
|
|
175
|
+
render:
|
|
176
|
+
template: default-list
|
|
177
|
+
formatRow: '{{title}} ({{status}}) — assigned to {{assignee.name}}'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The `formatRow` string uses `{{field}}` interpolation. Use `{{relationName.field}}` to pull in a field from a `belongsTo` related row.
|
|
181
|
+
|
|
182
|
+
### Form 3 — Default (omitted)
|
|
183
|
+
|
|
184
|
+
If `render` is omitted, it defaults to `default-list`.
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
entities:
|
|
188
|
+
note:
|
|
189
|
+
fields:
|
|
190
|
+
id: { type: uuid, primaryKey: true }
|
|
191
|
+
body: { type: text }
|
|
192
|
+
outputFile: context/NOTES.md
|
|
193
|
+
# render defaults to 'default-list'
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
See [Template Rendering](./templates.md) for the full guide.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Primary keys
|
|
201
|
+
|
|
202
|
+
### Field-level `primaryKey`
|
|
203
|
+
|
|
204
|
+
The simplest form — mark one field with `primaryKey: true`:
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
entities:
|
|
208
|
+
user:
|
|
209
|
+
fields:
|
|
210
|
+
id: { type: uuid, primaryKey: true }
|
|
211
|
+
name: { type: text }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
This sets the table's primary key to `id`. When inserting without an `id`, a UUID v4 is auto-generated.
|
|
215
|
+
|
|
216
|
+
### Custom single PK
|
|
217
|
+
|
|
218
|
+
Use a non-`id` field as the primary key:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
entities:
|
|
222
|
+
setting:
|
|
223
|
+
fields:
|
|
224
|
+
key: { type: text, primaryKey: true }
|
|
225
|
+
value: { type: text }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Callers must supply `key` on every insert. No UUID is auto-generated.
|
|
229
|
+
|
|
230
|
+
### Entity-level `primaryKey` override
|
|
231
|
+
|
|
232
|
+
For composite keys or when you want to separate the PK declaration from field definitions:
|
|
233
|
+
|
|
234
|
+
```yaml
|
|
235
|
+
entities:
|
|
236
|
+
line_item:
|
|
237
|
+
fields:
|
|
238
|
+
order_id: { type: uuid }
|
|
239
|
+
seq: { type: integer }
|
|
240
|
+
qty: { type: integer }
|
|
241
|
+
primaryKey: [order_id, seq]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
When entity-level `primaryKey` is set, it overrides any field-level `primaryKey: true`. The caller must supply all PK columns on every insert.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Output file paths
|
|
249
|
+
|
|
250
|
+
The `outputFile` path is resolved relative to the **config file's directory** (not `process.cwd()`).
|
|
251
|
+
|
|
252
|
+
```yaml
|
|
253
|
+
# lattice.config.yml lives at /project/lattice.config.yml
|
|
254
|
+
entities:
|
|
255
|
+
agent:
|
|
256
|
+
outputFile: context/AGENTS.md # → /project/context/AGENTS.md
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
This means the context files are co-located with the config file, regardless of where you run commands from.
|
|
260
|
+
|
|
261
|
+
When you call `db.render(outputDir)` or `db.watch(outputDir)` programmatically, the `outputFile` is resolved relative to `outputDir` — not the config file directory. The config file form resolves differently (see below).
|
|
262
|
+
|
|
263
|
+
> **Note:** When using `new Lattice({ config: '...' })`, the `outputFile` paths in the YAML are pre-resolved to absolute paths at parse time. `render()` / `watch()` receive these absolute paths and write there directly, ignoring `outputDir`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## Multiple entities
|
|
268
|
+
|
|
269
|
+
Define as many entities as needed. They are parsed and registered in the order they appear in the YAML:
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
db: ./data/app.db
|
|
273
|
+
entities:
|
|
274
|
+
user:
|
|
275
|
+
fields:
|
|
276
|
+
id: { type: uuid, primaryKey: true }
|
|
277
|
+
name: { type: text, required: true }
|
|
278
|
+
email: { type: text }
|
|
279
|
+
render: default-table
|
|
280
|
+
outputFile: context/USERS.md
|
|
281
|
+
|
|
282
|
+
project:
|
|
283
|
+
fields:
|
|
284
|
+
id: { type: uuid, primaryKey: true }
|
|
285
|
+
name: { type: text, required: true }
|
|
286
|
+
owner_id: { type: uuid, ref: user }
|
|
287
|
+
render:
|
|
288
|
+
template: default-list
|
|
289
|
+
formatRow: '{{name}} (owner: {{owner.name}})'
|
|
290
|
+
outputFile: context/PROJECTS.md
|
|
291
|
+
|
|
292
|
+
task:
|
|
293
|
+
fields:
|
|
294
|
+
id: { type: uuid, primaryKey: true }
|
|
295
|
+
title: { type: text, required: true }
|
|
296
|
+
status: { type: text, default: open }
|
|
297
|
+
priority: { type: integer, default: 1 }
|
|
298
|
+
project_id: { type: uuid, ref: project }
|
|
299
|
+
assignee_id: { type: uuid, ref: user }
|
|
300
|
+
render:
|
|
301
|
+
template: default-list
|
|
302
|
+
formatRow: '{{title}} [{{status}}] → {{assignee.name}}'
|
|
303
|
+
outputFile: context/TASKS.md
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Programmatic config API
|
|
309
|
+
|
|
310
|
+
You can use the parser directly without going through the `Lattice` constructor:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { parseConfigFile, parseConfigString } from 'latticesql';
|
|
314
|
+
|
|
315
|
+
// From a file:
|
|
316
|
+
const { dbPath, tables } = parseConfigFile('./lattice.config.yml');
|
|
317
|
+
|
|
318
|
+
// From a string (useful in tests):
|
|
319
|
+
const { dbPath, tables } = parseConfigString(yamlContent, '/path/to/config/dir');
|
|
320
|
+
|
|
321
|
+
// tables is: ReadonlyArray<{ name: string; definition: TableDefinition }>
|
|
322
|
+
for (const { name, definition } of tables) {
|
|
323
|
+
db.define(name, definition);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Both functions throw with descriptive messages on validation errors.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Complete example
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
# lattice.config.yml
|
|
335
|
+
db: ./data/app.db
|
|
336
|
+
|
|
337
|
+
entities:
|
|
338
|
+
# --- Users ---
|
|
339
|
+
user:
|
|
340
|
+
fields:
|
|
341
|
+
id: { type: uuid, primaryKey: true }
|
|
342
|
+
name: { type: text, required: true }
|
|
343
|
+
email: { type: text, required: true }
|
|
344
|
+
role: { type: text, default: member }
|
|
345
|
+
created_at: { type: datetime }
|
|
346
|
+
render: default-table
|
|
347
|
+
outputFile: context/USERS.md
|
|
348
|
+
|
|
349
|
+
# --- Projects ---
|
|
350
|
+
project:
|
|
351
|
+
fields:
|
|
352
|
+
id: { type: uuid, primaryKey: true }
|
|
353
|
+
name: { type: text, required: true }
|
|
354
|
+
status: { type: text, default: active }
|
|
355
|
+
owner_id: { type: uuid, ref: user }
|
|
356
|
+
render:
|
|
357
|
+
template: default-list
|
|
358
|
+
formatRow: '**{{name}}** [{{status}}] — {{owner.name}}'
|
|
359
|
+
outputFile: context/PROJECTS.md
|
|
360
|
+
|
|
361
|
+
# --- Tasks ---
|
|
362
|
+
task:
|
|
363
|
+
fields:
|
|
364
|
+
id: { type: uuid, primaryKey: true }
|
|
365
|
+
title: { type: text, required: true }
|
|
366
|
+
description: { type: text }
|
|
367
|
+
status: { type: text, default: open }
|
|
368
|
+
priority: { type: integer, default: 1 }
|
|
369
|
+
project_id: { type: uuid, ref: project }
|
|
370
|
+
assignee_id: { type: uuid, ref: user }
|
|
371
|
+
created_at: { type: datetime }
|
|
372
|
+
due_at: { type: datetime }
|
|
373
|
+
render:
|
|
374
|
+
template: default-list
|
|
375
|
+
formatRow: '{{title}} [P{{priority}}/{{status}}] → {{assignee.name}}'
|
|
376
|
+
outputFile: context/TASKS.md
|
|
377
|
+
|
|
378
|
+
# --- Comments ---
|
|
379
|
+
comment:
|
|
380
|
+
fields:
|
|
381
|
+
id: { type: uuid, primaryKey: true }
|
|
382
|
+
body: { type: text, required: true }
|
|
383
|
+
task_id: { type: uuid, ref: task }
|
|
384
|
+
author_id: { type: uuid, ref: user }
|
|
385
|
+
created_at: { type: datetime }
|
|
386
|
+
render:
|
|
387
|
+
template: default-list
|
|
388
|
+
formatRow: '{{author.name}}: {{body}}'
|
|
389
|
+
outputFile: context/COMMENTS.md
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Run `lattice generate` to produce TypeScript interfaces and a SQL migration file from this config. Then use `new Lattice({ config: './lattice.config.yml' })` at runtime to connect, define tables, and start syncing.
|
|
393
|
+
|
|
394
|
+
## Full-text search (`fts`)
|
|
395
|
+
|
|
396
|
+
Opt a table into an indexed full-text search by adding an `fts` block to its
|
|
397
|
+
entity definition. Omit `fields` to auto-detect text columns (excluding
|
|
398
|
+
identifiers and bookkeeping columns):
|
|
399
|
+
|
|
400
|
+
```yaml
|
|
401
|
+
entities:
|
|
402
|
+
articles:
|
|
403
|
+
fields:
|
|
404
|
+
id: { type: uuid, primaryKey: true }
|
|
405
|
+
title: { type: text }
|
|
406
|
+
body: { type: text }
|
|
407
|
+
fts: { fields: [title, body] } # or just `fts: {}` to auto-detect
|
|
408
|
+
outputFile: articles.md
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
On `init`, Lattice builds and maintains an inverted index
|
|
412
|
+
(`__lattice_fts_<table>`) — SQLite FTS5 / Postgres `tsvector` + GIN. Tables
|
|
413
|
+
without `fts` are still searchable via the LIKE fallback. Indexes are created
|
|
414
|
+
**only** for opt-in tables, so a library consumer with no `fts` config incurs
|
|
415
|
+
no index and no write-path overhead. See `docs/api-reference.md` →
|
|
416
|
+
_Full-text search_ for the `fullTextSearch` API.
|