latticesql 1.13.0 → 1.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -6
- package/dist/cli.js +155 -55
- package/dist/index.cjs +18 -1
- package/dist/index.d.cts +10 -2
- package/dist/index.d.ts +10 -2
- package/dist/index.js +18 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2066,12 +2066,25 @@ npx lattice gui --config ./lattice.config.yml --output ./context --port 4317
|
|
|
2066
2066
|
|
|
2067
2067
|
**Options**
|
|
2068
2068
|
|
|
2069
|
-
| Flag | Default | Description
|
|
2070
|
-
| --------------------- | ---------------------- |
|
|
2071
|
-
| `--config, -c <path>` | `./lattice.config.yml` | Path to the config file
|
|
2072
|
-
| `--output <dir>` |
|
|
2073
|
-
| `--port <number>` | `4317` | Localhost port; auto-increments when the port is busy
|
|
2074
|
-
| `--no-open` | off | Print the URL without opening a browser
|
|
2069
|
+
| Flag | Default | Description |
|
|
2070
|
+
| --------------------- | ---------------------- | -------------------------------------------------------- |
|
|
2071
|
+
| `--config, -c <path>` | `./lattice.config.yml` | Path to the config file |
|
|
2072
|
+
| `--output <dir>` | (auto-detected) | Output directory containing rendered context — see below |
|
|
2073
|
+
| `--port <number>` | `4317` | Localhost port; auto-increments when the port is busy |
|
|
2074
|
+
| `--no-open` | off | Print the URL without opening a browser |
|
|
2075
|
+
|
|
2076
|
+
**Output-directory auto-detection (v1.13.1+).** When `--output` is not passed explicitly, the GUI probes `./context`, `.`, and `./generated` in order and uses the first directory containing a `.lattice/manifest.json` (announced via a one-line `auto-detected rendered context at "<dir>"` log on stdout). Projects whose `lattice render` writes into the project root no longer need to pass `--output .` every time. An explicit `--output` is always honoured.
|
|
2077
|
+
|
|
2078
|
+
**Entity-context discovery (v1.13.1+).** The Database panel's row-context viewer reads entity contexts from two layered sources so it works regardless of how you register them:
|
|
2079
|
+
|
|
2080
|
+
1. **Live Lattice schema** — anything declared in `lattice.config.yml` or added programmatically via `db.defineEntityContext()` against the active Lattice. Exposed via the new public `Lattice.entityContexts()` accessor.
|
|
2081
|
+
2. **Render manifest fallback** — when a table has no schema-registered entity context but the on-disk `.lattice/manifest.json` names it (typical for projects that register entity contexts in a JS / TS module like `lattice.schema.mjs` that the GUI process never imports), the GUI derives the row → slug mapping heuristically from `row.slug` / `row.id` / `row.name` and surfaces the rendered files anyway.
|
|
2082
|
+
|
|
2083
|
+
The convergence means you don't need to duplicate entity-context definitions in YAML for the GUI to find rendered files.
|
|
2084
|
+
|
|
2085
|
+
**Database wizard form (v1.13.2+).** The Postgres connection form (used by Migrate to cloud + Connect to existing cloud) disables browser autocapitalize, autocorrect, and spellcheck on every text input, and trims whitespace on every read. This avoids silent failure modes where macOS Safari / iOS turned a Supabase tenant user `postgres.<ref>` into `Postgres.<ref>` on submit, and where pasted credentials carrying a trailing newline produced opaque "zero-length delimiter identifier" or SCRAM-mismatch errors. `probeCloud` also folds SQLSTATE + `routine` into `result.error` so the GUI's "Unreachable: …" surface is actionable.
|
|
2086
|
+
|
|
2087
|
+
**Switch vs. migrate (v1.13.2+ wording).** "Connect to existing cloud" _switches_ the project's `db:` line to point at the cloud; the local SQLite file stays on disk and you can switch back by editing the YAML or via the Databases catalog under User Config. Use "Migrate to cloud" only when you want to _push_ the local data into a fresh empty target.
|
|
2075
2088
|
|
|
2076
2089
|
**Views**
|
|
2077
2090
|
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
|
-
import { resolve as
|
|
10
|
+
import { resolve as resolve9, dirname as dirname7 } from "path";
|
|
11
11
|
import { readFileSync as readFileSync12 } from "fs";
|
|
12
12
|
import { execSync } from "child_process";
|
|
13
13
|
import { parse as parse2 } from "yaml";
|
|
@@ -3369,6 +3369,16 @@ var Lattice = class {
|
|
|
3369
3369
|
this._schema.defineEntityContext(table, def);
|
|
3370
3370
|
return this;
|
|
3371
3371
|
}
|
|
3372
|
+
/**
|
|
3373
|
+
* All entity contexts currently registered on this Lattice — both those
|
|
3374
|
+
* declared in `lattice.config.yml` and those added programmatically via
|
|
3375
|
+
* `defineEntityContext()`.
|
|
3376
|
+
*
|
|
3377
|
+
* Returns a defensive copy so callers can't mutate the schema.
|
|
3378
|
+
*/
|
|
3379
|
+
entityContexts() {
|
|
3380
|
+
return new Map(this._schema.getEntityContexts());
|
|
3381
|
+
}
|
|
3372
3382
|
/**
|
|
3373
3383
|
* Register a write hook that fires after insert/update/delete operations.
|
|
3374
3384
|
* Hooks run synchronously after the DB write and audit emit.
|
|
@@ -5358,8 +5368,13 @@ var guiAppHtml = `<!doctype html>
|
|
|
5358
5368
|
}
|
|
5359
5369
|
|
|
5360
5370
|
/* \u2500\u2500 Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
5371
|
+
/* minmax(0, 1fr) on the content track lets a wide child (a table with
|
|
5372
|
+
chip-heavy cells) shrink instead of forcing the page wider than the
|
|
5373
|
+
viewport. Without the explicit 0 lower bound, the implicit auto
|
|
5374
|
+
minimum keeps the track at content-width and the whole page scrolls
|
|
5375
|
+
horizontally. */
|
|
5361
5376
|
.layout {
|
|
5362
|
-
display: grid; grid-template-columns: 220px 1fr;
|
|
5377
|
+
display: grid; grid-template-columns: 220px minmax(0, 1fr);
|
|
5363
5378
|
height: calc(100vh - 56px);
|
|
5364
5379
|
}
|
|
5365
5380
|
nav.sidebar {
|
|
@@ -5431,6 +5446,19 @@ var guiAppHtml = `<!doctype html>
|
|
|
5431
5446
|
tbody tr { cursor: pointer; }
|
|
5432
5447
|
tbody tr:hover td { background: var(--row-hover); }
|
|
5433
5448
|
td.muted { color: var(--text-muted); }
|
|
5449
|
+
/* Row cells truncate at 3 lines so a row with many chips or a long text
|
|
5450
|
+
blob stays one consistent visual height instead of wrapping into a
|
|
5451
|
+
paragraph. The wrapping <div class="cell-clip"> is necessary because
|
|
5452
|
+
-webkit-line-clamp doesn't apply to <td> directly in all engines. */
|
|
5453
|
+
td .cell-clip {
|
|
5454
|
+
display: -webkit-box;
|
|
5455
|
+
-webkit-line-clamp: 3;
|
|
5456
|
+
-webkit-box-orient: vertical;
|
|
5457
|
+
overflow: hidden;
|
|
5458
|
+
line-height: 1.45;
|
|
5459
|
+
max-height: calc(1.45em * 3);
|
|
5460
|
+
word-break: break-word;
|
|
5461
|
+
}
|
|
5434
5462
|
.chip {
|
|
5435
5463
|
display: inline-block; padding: 2px 8px; margin: 1px 3px 1px 0;
|
|
5436
5464
|
background: var(--accent-soft); color: var(--accent);
|
|
@@ -6390,11 +6418,11 @@ var guiAppHtml = `<!doctype html>
|
|
|
6390
6418
|
if (isSecretColumn(tableName, c) && r[c] != null && r[c] !== '') {
|
|
6391
6419
|
return '<td class="muted">' + SECRET_MASK + '</td>';
|
|
6392
6420
|
}
|
|
6393
|
-
return '<td>' + escapeHtml(truncate(r[c], 120)) + '</td>';
|
|
6421
|
+
return '<td><div class="cell-clip">' + escapeHtml(truncate(r[c], 120)) + '</div></td>';
|
|
6394
6422
|
});
|
|
6395
6423
|
belongsTo.forEach(function (b) {
|
|
6396
6424
|
var ref = (loadedTables[b.rel.table] || []).find(function (x) { return x.id === r[b.rel.foreignKey]; });
|
|
6397
|
-
tds.push('<td>' + chipLink(b.rel.table, ref) + '</td>');
|
|
6425
|
+
tds.push('<td><div class="cell-clip">' + chipLink(b.rel.table, ref) + '</div></td>');
|
|
6398
6426
|
});
|
|
6399
6427
|
junctions.forEach(function (j) {
|
|
6400
6428
|
var matches = (loadedTables[j.junction] || []).filter(function (jr) { return jr[j.localFk] === r.id; });
|
|
@@ -6403,7 +6431,7 @@ var guiAppHtml = `<!doctype html>
|
|
|
6403
6431
|
var ref = (loadedTables[j.remoteRel.table] || []).find(function (x) { return x.id === jr[remoteFkCol]; });
|
|
6404
6432
|
return ref ? chipLink(j.remoteRel.table, ref) : '';
|
|
6405
6433
|
}).join('');
|
|
6406
|
-
tds.push('<td>' + (chips || '<span class="muted">\u2014</span>') + '</td>');
|
|
6434
|
+
tds.push('<td><div class="cell-clip">' + (chips || '<span class="muted">\u2014</span>') + '</div></td>');
|
|
6407
6435
|
});
|
|
6408
6436
|
if (viewMode === 'trash') {
|
|
6409
6437
|
tds.push('<td class="row-actions">' +
|
|
@@ -7971,27 +7999,39 @@ var guiAppHtml = `<!doctype html>
|
|
|
7971
7999
|
|
|
7972
8000
|
function postgresFormHtml(prefill) {
|
|
7973
8001
|
prefill = prefill || {};
|
|
8002
|
+
// autocapitalize="off" + autocorrect="off" + spellcheck="false" keep
|
|
8003
|
+
// mobile / macOS keyboards from "helpfully" capitalizing the first
|
|
8004
|
+
// letter of usernames + host fragments. Supabase tenant users
|
|
8005
|
+
// (postgres.<ref>) are case-sensitive and silently failed
|
|
8006
|
+
// authentication when iOS Safari turned the leading "p" into "P".
|
|
8007
|
+
var attrs = ' autocapitalize="off" autocorrect="off" spellcheck="false"';
|
|
7974
8008
|
return (
|
|
7975
8009
|
'<div class="grid" style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">' +
|
|
7976
|
-
'<div><label class="field-label">Label</label><input type="text" id="w-label" placeholder="atlas" value="' + escapeHtml(prefill.label || '') + '" style="width:100%"></div>' +
|
|
7977
|
-
'<div><label class="field-label">Host</label><input type="text" id="w-host" placeholder="db.example.com" value="' + escapeHtml(prefill.host || '') + '" style="width:100%"></div>' +
|
|
8010
|
+
'<div><label class="field-label">Label</label><input type="text" id="w-label" placeholder="atlas" value="' + escapeHtml(prefill.label || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
8011
|
+
'<div><label class="field-label">Host</label><input type="text" id="w-host" placeholder="db.example.com" value="' + escapeHtml(prefill.host || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
7978
8012
|
'<div><label class="field-label">Port</label><input type="number" id="w-port" placeholder="5432" value="' + escapeHtml(String(prefill.port || 5432)) + '" style="width:100%"></div>' +
|
|
7979
|
-
'<div><label class="field-label">Database name</label><input type="text" id="w-dbname" placeholder="app" value="' + escapeHtml(prefill.dbname || '') + '" style="width:100%"></div>' +
|
|
7980
|
-
'<div><label class="field-label">User</label><input type="text" id="w-user" placeholder="lattice_user" value="' + escapeHtml(prefill.user || '') + '" style="width:100%"></div>' +
|
|
7981
|
-
'<div><label class="field-label">Password</label><input type="password" id="w-password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" style="width:100%"></div>' +
|
|
8013
|
+
'<div><label class="field-label">Database name</label><input type="text" id="w-dbname" placeholder="app" value="' + escapeHtml(prefill.dbname || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
8014
|
+
'<div><label class="field-label">User</label><input type="text" id="w-user" placeholder="lattice_user" value="' + escapeHtml(prefill.user || '') + '" style="width:100%"' + attrs + '></div>' +
|
|
8015
|
+
'<div><label class="field-label">Password</label><input type="password" id="w-password" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" style="width:100%"' + attrs + '></div>' +
|
|
7982
8016
|
'</div>'
|
|
7983
8017
|
);
|
|
7984
8018
|
}
|
|
7985
8019
|
|
|
7986
8020
|
function readPostgresWizardForm() {
|
|
8021
|
+
// Every text field is trimmed \u2014 pasted credentials frequently carry a
|
|
8022
|
+
// trailing newline or leading space that breaks URL construction
|
|
8023
|
+
// (zero-length identifier errors from the Postgres parser) or SCRAM
|
|
8024
|
+
// auth (silent password mismatch). Trim once, here, so every caller
|
|
8025
|
+
// benefits.
|
|
8026
|
+
var get = function (id) { return (document.getElementById(id).value || '').trim(); };
|
|
7987
8027
|
return {
|
|
7988
8028
|
type: 'postgres',
|
|
7989
|
-
label: (
|
|
7990
|
-
host: (
|
|
8029
|
+
label: get('w-label'),
|
|
8030
|
+
host: get('w-host'),
|
|
7991
8031
|
port: Number(document.getElementById('w-port').value || 5432),
|
|
7992
|
-
dbname: (
|
|
7993
|
-
user:
|
|
7994
|
-
password:
|
|
8032
|
+
dbname: get('w-dbname'),
|
|
8033
|
+
user: get('w-user'),
|
|
8034
|
+
password: get('w-password'),
|
|
7995
8035
|
};
|
|
7996
8036
|
}
|
|
7997
8037
|
|
|
@@ -8026,10 +8066,14 @@ var guiAppHtml = `<!doctype html>
|
|
|
8026
8066
|
function showConnectExistingModal(onClose) {
|
|
8027
8067
|
var bodyHtml =
|
|
8028
8068
|
'<p style="margin:0 0 12px;font-size:13px;color:var(--text-muted)">' +
|
|
8029
|
-
'
|
|
8030
|
-
'Your local SQLite
|
|
8031
|
-
'
|
|
8032
|
-
'
|
|
8069
|
+
'Switch this project to an <strong>existing</strong> cloud Postgres. ' +
|
|
8070
|
+
'Your local SQLite file is preserved \u2014 only this project\\'s active ' +
|
|
8071
|
+
'connection changes. Switch back any time by editing ' +
|
|
8072
|
+
'<code>lattice.config.yml</code>\\'s <code>db:</code> line or via the ' +
|
|
8073
|
+
'Databases catalog under User Config. If you want to <em>push</em> ' +
|
|
8074
|
+
'your local rows into the target instead, use Migrate to cloud. If ' +
|
|
8075
|
+
'the target is a teams DB you\\'ll be asked for an invite token ' +
|
|
8076
|
+
'after the probe.' +
|
|
8033
8077
|
'</p>' +
|
|
8034
8078
|
postgresFormHtml({}) +
|
|
8035
8079
|
'<div id="w-team-zone" style="margin-top:10px"></div>' +
|
|
@@ -8703,7 +8747,7 @@ function sendJson(res, body, status = 200) {
|
|
|
8703
8747
|
res.end(JSON.stringify(body));
|
|
8704
8748
|
}
|
|
8705
8749
|
function readJson(req) {
|
|
8706
|
-
return new Promise((
|
|
8750
|
+
return new Promise((resolve10, reject) => {
|
|
8707
8751
|
let raw = "";
|
|
8708
8752
|
req.setEncoding("utf8");
|
|
8709
8753
|
req.on("data", (chunk) => {
|
|
@@ -8712,7 +8756,7 @@ function readJson(req) {
|
|
|
8712
8756
|
});
|
|
8713
8757
|
req.on("end", () => {
|
|
8714
8758
|
try {
|
|
8715
|
-
|
|
8759
|
+
resolve10(raw ? JSON.parse(raw) : {});
|
|
8716
8760
|
} catch (e) {
|
|
8717
8761
|
reject(new Error(`Invalid JSON body: ${e.message}`));
|
|
8718
8762
|
}
|
|
@@ -9720,11 +9764,18 @@ async function probeCloud(targetUrl) {
|
|
|
9720
9764
|
}
|
|
9721
9765
|
return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
|
|
9722
9766
|
} catch (e) {
|
|
9767
|
+
const err = e;
|
|
9768
|
+
const parts = [];
|
|
9769
|
+
if (err.code) parts.push(`[${err.code}]`);
|
|
9770
|
+
if (err.message) parts.push(err.message);
|
|
9771
|
+
if (err.routine && !err.message.includes(err.routine)) {
|
|
9772
|
+
parts.push(`(routine: ${err.routine})`);
|
|
9773
|
+
}
|
|
9723
9774
|
return {
|
|
9724
9775
|
reachable: false,
|
|
9725
9776
|
dialect,
|
|
9726
9777
|
teamEnabled: false,
|
|
9727
|
-
error:
|
|
9778
|
+
error: parts.join(" ") || "unknown"
|
|
9728
9779
|
};
|
|
9729
9780
|
} finally {
|
|
9730
9781
|
if (probe) {
|
|
@@ -10403,7 +10454,7 @@ function sendJson2(res, body, status = 200) {
|
|
|
10403
10454
|
res.end(JSON.stringify(body));
|
|
10404
10455
|
}
|
|
10405
10456
|
function readJson2(req) {
|
|
10406
|
-
return new Promise((
|
|
10457
|
+
return new Promise((resolve10, reject) => {
|
|
10407
10458
|
let raw = "";
|
|
10408
10459
|
req.setEncoding("utf8");
|
|
10409
10460
|
req.on("data", (chunk) => {
|
|
@@ -10412,7 +10463,7 @@ function readJson2(req) {
|
|
|
10412
10463
|
});
|
|
10413
10464
|
req.on("end", () => {
|
|
10414
10465
|
try {
|
|
10415
|
-
|
|
10466
|
+
resolve10(raw ? JSON.parse(raw) : {});
|
|
10416
10467
|
} catch (e) {
|
|
10417
10468
|
reject(new Error(`Invalid JSON body: ${e.message}`));
|
|
10418
10469
|
}
|
|
@@ -10676,7 +10727,7 @@ function sendJson3(res, body, status = 200) {
|
|
|
10676
10727
|
res.end(JSON.stringify(body));
|
|
10677
10728
|
}
|
|
10678
10729
|
function readJson3(req) {
|
|
10679
|
-
return new Promise((
|
|
10730
|
+
return new Promise((resolve10, reject) => {
|
|
10680
10731
|
let raw = "";
|
|
10681
10732
|
req.setEncoding("utf8");
|
|
10682
10733
|
req.on("data", (chunk) => {
|
|
@@ -10685,7 +10736,7 @@ function readJson3(req) {
|
|
|
10685
10736
|
});
|
|
10686
10737
|
req.on("end", () => {
|
|
10687
10738
|
try {
|
|
10688
|
-
|
|
10739
|
+
resolve10(raw ? JSON.parse(raw) : {});
|
|
10689
10740
|
} catch (e) {
|
|
10690
10741
|
reject(new Error(`Invalid JSON body: ${e.message}`));
|
|
10691
10742
|
}
|
|
@@ -11520,15 +11571,43 @@ async function applyForward(db, entry) {
|
|
|
11520
11571
|
}
|
|
11521
11572
|
void before;
|
|
11522
11573
|
}
|
|
11523
|
-
function
|
|
11524
|
-
const
|
|
11525
|
-
const
|
|
11574
|
+
function deriveSlugFromManifest(row, knownSlugs) {
|
|
11575
|
+
const candidateFields = ["slug", "id", "name"];
|
|
11576
|
+
for (const field of candidateFields) {
|
|
11577
|
+
const value = row[field];
|
|
11578
|
+
if (typeof value === "string" && knownSlugs.has(value)) return value;
|
|
11579
|
+
}
|
|
11580
|
+
return null;
|
|
11581
|
+
}
|
|
11582
|
+
function buildRowContextLocator(table, row, schemaDef, manifest) {
|
|
11583
|
+
if (schemaDef) {
|
|
11584
|
+
return {
|
|
11585
|
+
directoryRoot: schemaDef.directoryRoot ?? "",
|
|
11586
|
+
slug: schemaDef.slug(row),
|
|
11587
|
+
fileNames: Object.keys(schemaDef.files)
|
|
11588
|
+
};
|
|
11589
|
+
}
|
|
11590
|
+
const manifestEntry = manifest?.entityContexts[table];
|
|
11591
|
+
if (!manifestEntry) return null;
|
|
11592
|
+
const knownSlugs = new Set(Object.keys(manifestEntry.entities));
|
|
11593
|
+
const derivedSlug = deriveSlugFromManifest(row, knownSlugs);
|
|
11594
|
+
if (!derivedSlug) return null;
|
|
11595
|
+
const entityFiles = manifestEntry.entities[derivedSlug];
|
|
11596
|
+
const fileNames = entityFiles ? entityFileNames(entityFiles) : manifestEntry.declaredFiles;
|
|
11597
|
+
return {
|
|
11598
|
+
directoryRoot: manifestEntry.directoryRoot,
|
|
11599
|
+
slug: derivedSlug,
|
|
11600
|
+
fileNames
|
|
11601
|
+
};
|
|
11602
|
+
}
|
|
11603
|
+
function readRowContext(outputDir, locator, secretCols) {
|
|
11604
|
+
const { slug, directoryRoot, fileNames } = locator;
|
|
11526
11605
|
const entityDir = resolve6(outputDir, directoryRoot, slug);
|
|
11527
11606
|
const resolvedBase = resolve6(outputDir);
|
|
11528
11607
|
if (entityDir !== resolvedBase && !entityDir.startsWith(resolvedBase + sep3)) {
|
|
11529
11608
|
throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
|
|
11530
11609
|
}
|
|
11531
|
-
return
|
|
11610
|
+
return fileNames.map((filename) => {
|
|
11532
11611
|
const absPath = join13(entityDir, filename);
|
|
11533
11612
|
const relPath = join13(directoryRoot, slug, filename);
|
|
11534
11613
|
if (!existsSync14(absPath)) return { name: filename, path: relPath, content: "" };
|
|
@@ -11598,10 +11677,8 @@ async function openConfig(configPath, outputDir) {
|
|
|
11598
11677
|
const junctionTables = new Set(
|
|
11599
11678
|
getGuiEntities(configPath, outputDir).tables.filter(isJunctionTable).map((t) => t.name)
|
|
11600
11679
|
);
|
|
11601
|
-
const entityContextByTable =
|
|
11602
|
-
|
|
11603
|
-
entityContextByTable.set(table, definition);
|
|
11604
|
-
}
|
|
11680
|
+
const entityContextByTable = db.entityContexts();
|
|
11681
|
+
const manifest = readManifest(outputDir);
|
|
11605
11682
|
const softDeletable = new Set(
|
|
11606
11683
|
parsed.tables.filter(({ definition }) => "deleted_at" in definition.columns).map(({ name }) => name)
|
|
11607
11684
|
);
|
|
@@ -11615,6 +11692,7 @@ async function openConfig(configPath, outputDir) {
|
|
|
11615
11692
|
validTables,
|
|
11616
11693
|
junctionTables,
|
|
11617
11694
|
entityContextByTable,
|
|
11695
|
+
manifest,
|
|
11618
11696
|
softDeletable
|
|
11619
11697
|
};
|
|
11620
11698
|
}
|
|
@@ -12116,16 +12194,17 @@ async function startGuiServer(options) {
|
|
|
12116
12194
|
sendJson5(res, { error: `Unknown table: ${ctxTable}` }, 400);
|
|
12117
12195
|
return;
|
|
12118
12196
|
}
|
|
12119
|
-
const def = active.entityContextByTable.get(ctxTable);
|
|
12120
|
-
if (!def) {
|
|
12121
|
-
sendJson5(res, { files: [] });
|
|
12122
|
-
return;
|
|
12123
|
-
}
|
|
12124
12197
|
const row = await active.db.get(ctxTable, ctxId);
|
|
12125
12198
|
if (row === null) {
|
|
12126
12199
|
sendJson5(res, { error: "Row not found" }, 404);
|
|
12127
12200
|
return;
|
|
12128
12201
|
}
|
|
12202
|
+
const def = active.entityContextByTable.get(ctxTable);
|
|
12203
|
+
const locator = buildRowContextLocator(ctxTable, row, def, active.manifest);
|
|
12204
|
+
if (!locator) {
|
|
12205
|
+
sendJson5(res, { files: [] });
|
|
12206
|
+
return;
|
|
12207
|
+
}
|
|
12129
12208
|
const colMetaRows = await active.db.query("_lattice_gui_column_meta", {
|
|
12130
12209
|
filters: [
|
|
12131
12210
|
{ col: "table_name", op: "eq", val: ctxTable },
|
|
@@ -12133,7 +12212,7 @@ async function startGuiServer(options) {
|
|
|
12133
12212
|
]
|
|
12134
12213
|
});
|
|
12135
12214
|
const secretCols = new Set(colMetaRows.map((r) => r.column_name));
|
|
12136
|
-
sendJson5(res, { files: readRowContext(active.outputDir,
|
|
12215
|
+
sendJson5(res, { files: readRowContext(active.outputDir, locator, secretCols) });
|
|
12137
12216
|
return;
|
|
12138
12217
|
}
|
|
12139
12218
|
const rowsMatch = ROWS_PATH.exec(pathname);
|
|
@@ -12288,8 +12367,20 @@ async function startGuiServer(options) {
|
|
|
12288
12367
|
};
|
|
12289
12368
|
}
|
|
12290
12369
|
|
|
12370
|
+
// src/gui/discover-output-dir.ts
|
|
12371
|
+
import { existsSync as existsSync15 } from "fs";
|
|
12372
|
+
import { join as join14, resolve as resolve7 } from "path";
|
|
12373
|
+
function discoverOutputDir(explicitOutput, explicit) {
|
|
12374
|
+
if (explicit) return explicitOutput;
|
|
12375
|
+
const candidates = ["./context", ".", "./generated"];
|
|
12376
|
+
for (const dir of candidates) {
|
|
12377
|
+
if (existsSync15(join14(resolve7(dir), ".lattice", "manifest.json"))) return dir;
|
|
12378
|
+
}
|
|
12379
|
+
return explicitOutput;
|
|
12380
|
+
}
|
|
12381
|
+
|
|
12291
12382
|
// src/teams/cli-commands.ts
|
|
12292
|
-
import { resolve as
|
|
12383
|
+
import { resolve as resolve8 } from "path";
|
|
12293
12384
|
var TEAMS_USAGE = [
|
|
12294
12385
|
"lattice teams <subcommand> [options]",
|
|
12295
12386
|
"",
|
|
@@ -12405,7 +12496,7 @@ function requireArg(args, key, label) {
|
|
|
12405
12496
|
return v.trim();
|
|
12406
12497
|
}
|
|
12407
12498
|
async function openLocal(configPath) {
|
|
12408
|
-
const db = new Lattice({ config:
|
|
12499
|
+
const db = new Lattice({ config: resolve8(configPath) });
|
|
12409
12500
|
await db.init();
|
|
12410
12501
|
return db;
|
|
12411
12502
|
}
|
|
@@ -12729,6 +12820,7 @@ function parseArgs(argv) {
|
|
|
12729
12820
|
let config = "./lattice.config.yml";
|
|
12730
12821
|
let out = "./generated";
|
|
12731
12822
|
let output = "./context";
|
|
12823
|
+
let outputExplicit = false;
|
|
12732
12824
|
let scaffold = false;
|
|
12733
12825
|
let help = false;
|
|
12734
12826
|
let version = false;
|
|
@@ -12779,6 +12871,7 @@ function parseArgs(argv) {
|
|
|
12779
12871
|
} else if ((arg === "--output" || arg === "--output-dir") && i + 1 < argv.length) {
|
|
12780
12872
|
i++;
|
|
12781
12873
|
output = argv[i] ?? output;
|
|
12874
|
+
outputExplicit = true;
|
|
12782
12875
|
} else if (arg === "--scaffold") {
|
|
12783
12876
|
scaffold = true;
|
|
12784
12877
|
} else if (arg === "--dry-run") {
|
|
@@ -12854,6 +12947,7 @@ function parseArgs(argv) {
|
|
|
12854
12947
|
config,
|
|
12855
12948
|
out,
|
|
12856
12949
|
output,
|
|
12950
|
+
outputExplicit,
|
|
12857
12951
|
scaffold,
|
|
12858
12952
|
help,
|
|
12859
12953
|
version,
|
|
@@ -12979,7 +13073,7 @@ async function runUpdate() {
|
|
|
12979
13073
|
}
|
|
12980
13074
|
}
|
|
12981
13075
|
function runGenerate(args) {
|
|
12982
|
-
const configPath =
|
|
13076
|
+
const configPath = resolve9(args.config);
|
|
12983
13077
|
let raw;
|
|
12984
13078
|
try {
|
|
12985
13079
|
raw = readFileSync12(configPath, "utf-8");
|
|
@@ -12999,7 +13093,7 @@ function runGenerate(args) {
|
|
|
12999
13093
|
process.exit(1);
|
|
13000
13094
|
}
|
|
13001
13095
|
const configDir2 = dirname7(configPath);
|
|
13002
|
-
const outDir =
|
|
13096
|
+
const outDir = resolve9(args.out);
|
|
13003
13097
|
try {
|
|
13004
13098
|
const result = generateAll({ config, configDir: configDir2, outDir, scaffold: args.scaffold });
|
|
13005
13099
|
console.log(`Generated ${String(result.filesWritten.length)} file(s):`);
|
|
@@ -13012,15 +13106,15 @@ function runGenerate(args) {
|
|
|
13012
13106
|
}
|
|
13013
13107
|
}
|
|
13014
13108
|
async function runRender(args) {
|
|
13015
|
-
const outputDir =
|
|
13109
|
+
const outputDir = resolve9(args.output);
|
|
13016
13110
|
let parsed;
|
|
13017
13111
|
try {
|
|
13018
|
-
parsed = parseConfigFile(
|
|
13112
|
+
parsed = parseConfigFile(resolve9(args.config));
|
|
13019
13113
|
} catch (e) {
|
|
13020
13114
|
console.error(`Error: ${e.message}`);
|
|
13021
13115
|
process.exit(1);
|
|
13022
13116
|
}
|
|
13023
|
-
const db = new Lattice({ config:
|
|
13117
|
+
const db = new Lattice({ config: resolve9(args.config) });
|
|
13024
13118
|
try {
|
|
13025
13119
|
await db.init();
|
|
13026
13120
|
const start = Date.now();
|
|
@@ -13039,8 +13133,8 @@ async function runRender(args) {
|
|
|
13039
13133
|
void parsed;
|
|
13040
13134
|
}
|
|
13041
13135
|
async function runReconcile(args, isDryRun) {
|
|
13042
|
-
const outputDir =
|
|
13043
|
-
const db = new Lattice({ config:
|
|
13136
|
+
const outputDir = resolve9(args.output);
|
|
13137
|
+
const db = new Lattice({ config: resolve9(args.config) });
|
|
13044
13138
|
try {
|
|
13045
13139
|
await db.init();
|
|
13046
13140
|
const start = Date.now();
|
|
@@ -13099,8 +13193,8 @@ function formatTimestamp() {
|
|
|
13099
13193
|
return `${hh}:${mm}:${ss}`;
|
|
13100
13194
|
}
|
|
13101
13195
|
async function runWatch(args) {
|
|
13102
|
-
const outputDir =
|
|
13103
|
-
const db = new Lattice({ config:
|
|
13196
|
+
const outputDir = resolve9(args.output);
|
|
13197
|
+
const db = new Lattice({ config: resolve9(args.config) });
|
|
13104
13198
|
try {
|
|
13105
13199
|
await db.init();
|
|
13106
13200
|
} catch (e) {
|
|
@@ -13141,9 +13235,15 @@ async function runWatch(args) {
|
|
|
13141
13235
|
}
|
|
13142
13236
|
async function runGui(args) {
|
|
13143
13237
|
try {
|
|
13238
|
+
const resolvedOutput = discoverOutputDir(args.output, args.outputExplicit);
|
|
13239
|
+
if (!args.outputExplicit && resolvedOutput !== args.output) {
|
|
13240
|
+
console.log(
|
|
13241
|
+
`Lattice GUI: auto-detected rendered context at "${resolvedOutput}" (use --output to override).`
|
|
13242
|
+
);
|
|
13243
|
+
}
|
|
13144
13244
|
const handle = await startGuiServer({
|
|
13145
|
-
configPath:
|
|
13146
|
-
outputDir:
|
|
13245
|
+
configPath: resolve9(args.config),
|
|
13246
|
+
outputDir: resolve9(resolvedOutput),
|
|
13147
13247
|
port: args.port,
|
|
13148
13248
|
openBrowser: !args.noOpen
|
|
13149
13249
|
});
|
|
@@ -13162,8 +13262,8 @@ async function runGui(args) {
|
|
|
13162
13262
|
async function runServe(args) {
|
|
13163
13263
|
try {
|
|
13164
13264
|
const handle = await startGuiServer({
|
|
13165
|
-
configPath:
|
|
13166
|
-
outputDir:
|
|
13265
|
+
configPath: resolve9(args.config),
|
|
13266
|
+
outputDir: resolve9(args.output),
|
|
13167
13267
|
host: args.host,
|
|
13168
13268
|
port: args.port,
|
|
13169
13269
|
openBrowser: false,
|
package/dist/index.cjs
CHANGED
|
@@ -3434,6 +3434,16 @@ var Lattice = class {
|
|
|
3434
3434
|
this._schema.defineEntityContext(table, def);
|
|
3435
3435
|
return this;
|
|
3436
3436
|
}
|
|
3437
|
+
/**
|
|
3438
|
+
* All entity contexts currently registered on this Lattice — both those
|
|
3439
|
+
* declared in `lattice.config.yml` and those added programmatically via
|
|
3440
|
+
* `defineEntityContext()`.
|
|
3441
|
+
*
|
|
3442
|
+
* Returns a defensive copy so callers can't mutate the schema.
|
|
3443
|
+
*/
|
|
3444
|
+
entityContexts() {
|
|
3445
|
+
return new Map(this._schema.getEntityContexts());
|
|
3446
|
+
}
|
|
3437
3447
|
/**
|
|
3438
3448
|
* Register a write hook that fires after insert/update/delete operations.
|
|
3439
3449
|
* Hooks run synchronously after the DB write and audit emit.
|
|
@@ -5720,11 +5730,18 @@ async function probeCloud(targetUrl) {
|
|
|
5720
5730
|
}
|
|
5721
5731
|
return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
|
|
5722
5732
|
} catch (e) {
|
|
5733
|
+
const err = e;
|
|
5734
|
+
const parts = [];
|
|
5735
|
+
if (err.code) parts.push(`[${err.code}]`);
|
|
5736
|
+
if (err.message) parts.push(err.message);
|
|
5737
|
+
if (err.routine && !err.message.includes(err.routine)) {
|
|
5738
|
+
parts.push(`(routine: ${err.routine})`);
|
|
5739
|
+
}
|
|
5723
5740
|
return {
|
|
5724
5741
|
reachable: false,
|
|
5725
5742
|
dialect,
|
|
5726
5743
|
teamEnabled: false,
|
|
5727
|
-
error:
|
|
5744
|
+
error: parts.join(" ") || "unknown"
|
|
5728
5745
|
};
|
|
5729
5746
|
} finally {
|
|
5730
5747
|
if (probe) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1608,6 +1608,14 @@ declare class Lattice {
|
|
|
1608
1608
|
private _registerTable;
|
|
1609
1609
|
defineMulti(name: string, def: MultiTableDefinition): this;
|
|
1610
1610
|
defineEntityContext(table: string, def: EntityContextDefinition): this;
|
|
1611
|
+
/**
|
|
1612
|
+
* All entity contexts currently registered on this Lattice — both those
|
|
1613
|
+
* declared in `lattice.config.yml` and those added programmatically via
|
|
1614
|
+
* `defineEntityContext()`.
|
|
1615
|
+
*
|
|
1616
|
+
* Returns a defensive copy so callers can't mutate the schema.
|
|
1617
|
+
*/
|
|
1618
|
+
entityContexts(): Map<string, EntityContextDefinition>;
|
|
1611
1619
|
/**
|
|
1612
1620
|
* Register a write hook that fires after insert/update/delete operations.
|
|
1613
1621
|
* Hooks run synchronously after the DB write and audit emit.
|
|
@@ -2731,8 +2739,8 @@ declare function attachBlob(srcPath: string, latticeRoot: string): Promise<BlobM
|
|
|
2731
2739
|
* keys/<label>.token per-joined-team bearer tokens (added later).
|
|
2732
2740
|
* db-credentials.enc encrypted Postgres URLs (added later).
|
|
2733
2741
|
*
|
|
2734
|
-
*
|
|
2735
|
-
*
|
|
2742
|
+
* Security: do NOT log filesystem paths or user identity values from
|
|
2743
|
+
* this module. Errors must be thrown without echoing sensitive arguments.
|
|
2736
2744
|
*/
|
|
2737
2745
|
/** Root directory for machine-local lattice config. Override via env. */
|
|
2738
2746
|
declare function configDir(): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1608,6 +1608,14 @@ declare class Lattice {
|
|
|
1608
1608
|
private _registerTable;
|
|
1609
1609
|
defineMulti(name: string, def: MultiTableDefinition): this;
|
|
1610
1610
|
defineEntityContext(table: string, def: EntityContextDefinition): this;
|
|
1611
|
+
/**
|
|
1612
|
+
* All entity contexts currently registered on this Lattice — both those
|
|
1613
|
+
* declared in `lattice.config.yml` and those added programmatically via
|
|
1614
|
+
* `defineEntityContext()`.
|
|
1615
|
+
*
|
|
1616
|
+
* Returns a defensive copy so callers can't mutate the schema.
|
|
1617
|
+
*/
|
|
1618
|
+
entityContexts(): Map<string, EntityContextDefinition>;
|
|
1611
1619
|
/**
|
|
1612
1620
|
* Register a write hook that fires after insert/update/delete operations.
|
|
1613
1621
|
* Hooks run synchronously after the DB write and audit emit.
|
|
@@ -2731,8 +2739,8 @@ declare function attachBlob(srcPath: string, latticeRoot: string): Promise<BlobM
|
|
|
2731
2739
|
* keys/<label>.token per-joined-team bearer tokens (added later).
|
|
2732
2740
|
* db-credentials.enc encrypted Postgres URLs (added later).
|
|
2733
2741
|
*
|
|
2734
|
-
*
|
|
2735
|
-
*
|
|
2742
|
+
* Security: do NOT log filesystem paths or user identity values from
|
|
2743
|
+
* this module. Errors must be thrown without echoing sensitive arguments.
|
|
2736
2744
|
*/
|
|
2737
2745
|
/** Root directory for machine-local lattice config. Override via env. */
|
|
2738
2746
|
declare function configDir(): string;
|
package/dist/index.js
CHANGED
|
@@ -3362,6 +3362,16 @@ var Lattice = class {
|
|
|
3362
3362
|
this._schema.defineEntityContext(table, def);
|
|
3363
3363
|
return this;
|
|
3364
3364
|
}
|
|
3365
|
+
/**
|
|
3366
|
+
* All entity contexts currently registered on this Lattice — both those
|
|
3367
|
+
* declared in `lattice.config.yml` and those added programmatically via
|
|
3368
|
+
* `defineEntityContext()`.
|
|
3369
|
+
*
|
|
3370
|
+
* Returns a defensive copy so callers can't mutate the schema.
|
|
3371
|
+
*/
|
|
3372
|
+
entityContexts() {
|
|
3373
|
+
return new Map(this._schema.getEntityContexts());
|
|
3374
|
+
}
|
|
3365
3375
|
/**
|
|
3366
3376
|
* Register a write hook that fires after insert/update/delete operations.
|
|
3367
3377
|
* Hooks run synchronously after the DB write and audit emit.
|
|
@@ -5648,11 +5658,18 @@ async function probeCloud(targetUrl) {
|
|
|
5648
5658
|
}
|
|
5649
5659
|
return teamName !== void 0 ? { reachable: true, dialect, teamEnabled, teamName } : { reachable: true, dialect, teamEnabled };
|
|
5650
5660
|
} catch (e) {
|
|
5661
|
+
const err = e;
|
|
5662
|
+
const parts = [];
|
|
5663
|
+
if (err.code) parts.push(`[${err.code}]`);
|
|
5664
|
+
if (err.message) parts.push(err.message);
|
|
5665
|
+
if (err.routine && !err.message.includes(err.routine)) {
|
|
5666
|
+
parts.push(`(routine: ${err.routine})`);
|
|
5667
|
+
}
|
|
5651
5668
|
return {
|
|
5652
5669
|
reachable: false,
|
|
5653
5670
|
dialect,
|
|
5654
5671
|
teamEnabled: false,
|
|
5655
|
-
error:
|
|
5672
|
+
error: parts.join(" ") || "unknown"
|
|
5656
5673
|
};
|
|
5657
5674
|
} finally {
|
|
5658
5675
|
if (probe) {
|
package/package.json
CHANGED