latticesql 3.1.0 → 3.2.1
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 +2174 -1016
- package/dist/index.cjs +393 -190
- package/dist/index.d.cts +15 -20
- package/dist/index.d.ts +15 -20
- package/dist/index.js +393 -190
- 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
package/dist/index.js
CHANGED
|
@@ -41443,37 +41443,63 @@ var THROTTLE_WINDOW_MS = 200;
|
|
|
41443
41443
|
var ProgressThrottle = class {
|
|
41444
41444
|
cb;
|
|
41445
41445
|
windowMs;
|
|
41446
|
-
|
|
41446
|
+
/**
|
|
41447
|
+
* Last passthrough time, keyed per table (`event.table`, or `''` for the
|
|
41448
|
+
* table-less `done`/`error` lifecycle events). Per-table — not a single shared
|
|
41449
|
+
* clock — so when tables render CONCURRENTLY each one keeps its own ~5/sec
|
|
41450
|
+
* budget: a fast table can't consume the window and starve a slow table's
|
|
41451
|
+
* progress. `force` (table-start) resets only that table's budget.
|
|
41452
|
+
*/
|
|
41453
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
41447
41454
|
constructor(cb, windowMs = THROTTLE_WINDOW_MS) {
|
|
41448
41455
|
this.cb = cb;
|
|
41449
41456
|
this.windowMs = windowMs;
|
|
41450
41457
|
}
|
|
41451
41458
|
/**
|
|
41452
|
-
* Emit a `table-progress` event, but only if the window since
|
|
41453
|
-
* passthrough has elapsed. Dropped events are simply not delivered — the
|
|
41454
|
-
* one that survives carries the latest running count.
|
|
41459
|
+
* Emit a `table-progress` event, but only if the window since this table's
|
|
41460
|
+
* last passthrough has elapsed. Dropped events are simply not delivered — the
|
|
41461
|
+
* next one that survives carries the latest running count.
|
|
41455
41462
|
*/
|
|
41456
41463
|
tick(event) {
|
|
41457
41464
|
if (!this.cb) return;
|
|
41465
|
+
const key = event.table ?? "";
|
|
41458
41466
|
const now = Date.now();
|
|
41459
|
-
if (now - this.lastEmit < this.windowMs) return;
|
|
41460
|
-
this.lastEmit
|
|
41467
|
+
if (now - (this.lastEmit.get(key) ?? 0) < this.windowMs) return;
|
|
41468
|
+
this.lastEmit.set(key, now);
|
|
41461
41469
|
this.cb(event);
|
|
41462
41470
|
}
|
|
41463
41471
|
/**
|
|
41464
|
-
* Emit a lifecycle event immediately and reset
|
|
41465
|
-
* `table-start`, `table-done`, `done`, and `error` — none of which
|
|
41466
|
-
* ever be dropped. Resetting on `table-start` gives each table a clean
|
|
41472
|
+
* Emit a lifecycle event immediately and reset this table's throttle window.
|
|
41473
|
+
* Use for `table-start`, `table-done`, `done`, and `error` — none of which
|
|
41474
|
+
* should ever be dropped. Resetting on `table-start` gives each table a clean
|
|
41475
|
+
* budget.
|
|
41467
41476
|
*/
|
|
41468
41477
|
force(event) {
|
|
41469
41478
|
if (!this.cb) return;
|
|
41470
|
-
this.lastEmit
|
|
41479
|
+
this.lastEmit.set(event.table ?? "", Date.now());
|
|
41471
41480
|
this.cb(event);
|
|
41472
41481
|
}
|
|
41473
41482
|
};
|
|
41474
41483
|
|
|
41484
|
+
// src/concurrency.ts
|
|
41485
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
41486
|
+
const results = new Array(items.length);
|
|
41487
|
+
let next = 0;
|
|
41488
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
41489
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
41490
|
+
for (; ; ) {
|
|
41491
|
+
const i6 = next++;
|
|
41492
|
+
if (i6 >= items.length) break;
|
|
41493
|
+
results[i6] = await fn(items[i6], i6);
|
|
41494
|
+
}
|
|
41495
|
+
});
|
|
41496
|
+
await Promise.all(workers);
|
|
41497
|
+
return results;
|
|
41498
|
+
}
|
|
41499
|
+
|
|
41475
41500
|
// src/render/engine.ts
|
|
41476
41501
|
var YIELD_EVERY_ENTITIES = 200;
|
|
41502
|
+
var RENDER_TABLE_CONCURRENCY = 4;
|
|
41477
41503
|
var NOOP_RENDER = () => "";
|
|
41478
41504
|
var RenderEngine = class {
|
|
41479
41505
|
_schema;
|
|
@@ -41655,163 +41681,175 @@ var RenderEngine = class {
|
|
|
41655
41681
|
* via `signal`.
|
|
41656
41682
|
*/
|
|
41657
41683
|
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
|
|
41658
|
-
const manifestData = {};
|
|
41659
41684
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
41660
41685
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
41661
41686
|
if (d6.protected) protectedTables.add(t8);
|
|
41662
41687
|
}
|
|
41663
41688
|
const entityTables = [...this._schema.getEntityContexts()];
|
|
41664
41689
|
const tableCount = entityTables.length;
|
|
41665
|
-
|
|
41666
|
-
|
|
41667
|
-
|
|
41668
|
-
|
|
41669
|
-
|
|
41670
|
-
const directoryRoot = def.directoryRoot ?? table;
|
|
41671
|
-
const entitiesTotal = allRows.length;
|
|
41672
|
-
throttle.force({
|
|
41673
|
-
kind: "table-start",
|
|
41674
|
-
table,
|
|
41675
|
-
entitiesRendered: 0,
|
|
41676
|
-
entitiesTotal,
|
|
41677
|
-
tableIndex,
|
|
41678
|
-
tableCount,
|
|
41679
|
-
pct: 0
|
|
41680
|
-
});
|
|
41681
|
-
const manifestEntry = {
|
|
41682
|
-
directoryRoot,
|
|
41683
|
-
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
41684
|
-
declaredFiles: Object.keys(def.files),
|
|
41685
|
-
protectedFiles: def.protectedFiles ?? [],
|
|
41686
|
-
entities: {}
|
|
41687
|
-
};
|
|
41688
|
-
if (def.index) {
|
|
41689
|
-
const indexPath = join4(outputDir, def.index.outputFile);
|
|
41690
|
-
if (atomicWrite(indexPath, def.index.render(allRows))) {
|
|
41691
|
-
filesWritten.push(indexPath);
|
|
41692
|
-
} else {
|
|
41693
|
-
counters.skipped++;
|
|
41694
|
-
}
|
|
41695
|
-
}
|
|
41696
|
-
for (let i6 = 0; i6 < allRows.length; i6++) {
|
|
41697
|
-
const entityRow = allRows[i6];
|
|
41690
|
+
if (signal?.aborted) return null;
|
|
41691
|
+
const renderedEntries = await mapWithConcurrency(
|
|
41692
|
+
entityTables,
|
|
41693
|
+
RENDER_TABLE_CONCURRENCY,
|
|
41694
|
+
async ([table, def], tableIndex) => {
|
|
41698
41695
|
if (signal?.aborted) return null;
|
|
41699
|
-
|
|
41700
|
-
|
|
41701
|
-
|
|
41702
|
-
const
|
|
41703
|
-
|
|
41704
|
-
|
|
41705
|
-
|
|
41706
|
-
|
|
41707
|
-
|
|
41708
|
-
|
|
41709
|
-
|
|
41710
|
-
|
|
41711
|
-
|
|
41712
|
-
|
|
41713
|
-
|
|
41714
|
-
|
|
41715
|
-
|
|
41716
|
-
|
|
41717
|
-
|
|
41718
|
-
|
|
41719
|
-
|
|
41720
|
-
|
|
41696
|
+
const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
41697
|
+
const allRows = await this._schema.queryTable(this._adapter, table);
|
|
41698
|
+
const directoryRoot = def.directoryRoot ?? table;
|
|
41699
|
+
const entitiesTotal = allRows.length;
|
|
41700
|
+
throttle.force({
|
|
41701
|
+
kind: "table-start",
|
|
41702
|
+
table,
|
|
41703
|
+
entitiesRendered: 0,
|
|
41704
|
+
entitiesTotal,
|
|
41705
|
+
tableIndex,
|
|
41706
|
+
tableCount,
|
|
41707
|
+
pct: 0
|
|
41708
|
+
});
|
|
41709
|
+
const manifestEntry = {
|
|
41710
|
+
directoryRoot,
|
|
41711
|
+
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
41712
|
+
declaredFiles: Object.keys(def.files),
|
|
41713
|
+
protectedFiles: def.protectedFiles ?? [],
|
|
41714
|
+
entities: {}
|
|
41715
|
+
};
|
|
41716
|
+
if (def.index) {
|
|
41717
|
+
const indexPath = join4(outputDir, def.index.outputFile);
|
|
41718
|
+
if (atomicWrite(indexPath, def.index.render(allRows))) {
|
|
41719
|
+
filesWritten.push(indexPath);
|
|
41720
|
+
} else {
|
|
41721
|
+
counters.skipped++;
|
|
41722
|
+
}
|
|
41723
|
+
}
|
|
41724
|
+
for (let i6 = 0; i6 < allRows.length; i6++) {
|
|
41725
|
+
const entityRow = allRows[i6];
|
|
41726
|
+
if (signal?.aborted) return null;
|
|
41727
|
+
if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
|
|
41728
|
+
await new Promise((r6) => setImmediate(r6));
|
|
41729
|
+
}
|
|
41730
|
+
const rawSlug = def.slug(entityRow);
|
|
41731
|
+
const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
|
|
41732
|
+
if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
|
|
41733
|
+
throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
|
|
41734
|
+
}
|
|
41735
|
+
const entityDir = def.directory ? join4(outputDir, def.directory(entityRow)) : join4(outputDir, directoryRoot, slug);
|
|
41736
|
+
const resolvedDir = resolve(entityDir);
|
|
41737
|
+
const resolvedBase = resolve(outputDir);
|
|
41738
|
+
if (!resolvedDir.startsWith(resolvedBase + sep) && resolvedDir !== resolvedBase) {
|
|
41739
|
+
throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
|
|
41740
|
+
}
|
|
41741
|
+
mkdirSync2(entityDir, { recursive: true });
|
|
41742
|
+
if (def.attachFileColumn) {
|
|
41743
|
+
const filePath = entityRow[def.attachFileColumn];
|
|
41744
|
+
if (filePath && typeof filePath === "string" && filePath.length > 0) {
|
|
41745
|
+
if (def.attachFileMode === "reference") {
|
|
41746
|
+
const refPath = join4(entityDir, `${basename(filePath)}.ref.md`);
|
|
41747
|
+
try {
|
|
41748
|
+
atomicWrite(refPath, `# Reference
|
|
41721
41749
|
|
|
41722
41750
|
- **location:** ${filePath}
|
|
41723
41751
|
`);
|
|
41724
|
-
|
|
41725
|
-
|
|
41726
|
-
|
|
41727
|
-
|
|
41728
|
-
|
|
41729
|
-
|
|
41730
|
-
|
|
41731
|
-
|
|
41732
|
-
|
|
41733
|
-
|
|
41734
|
-
|
|
41735
|
-
|
|
41752
|
+
filesWritten.push(refPath);
|
|
41753
|
+
} catch {
|
|
41754
|
+
}
|
|
41755
|
+
} else {
|
|
41756
|
+
const absPath = isAbsolute(filePath) ? filePath : resolve(outputDir, filePath);
|
|
41757
|
+
if (existsSync4(absPath)) {
|
|
41758
|
+
const destPath = join4(entityDir, basename(absPath));
|
|
41759
|
+
if (!existsSync4(destPath)) {
|
|
41760
|
+
try {
|
|
41761
|
+
copyFileSync2(absPath, destPath);
|
|
41762
|
+
filesWritten.push(destPath);
|
|
41763
|
+
} catch {
|
|
41764
|
+
}
|
|
41736
41765
|
}
|
|
41737
41766
|
}
|
|
41738
41767
|
}
|
|
41739
41768
|
}
|
|
41740
41769
|
}
|
|
41741
|
-
|
|
41742
|
-
|
|
41743
|
-
|
|
41744
|
-
|
|
41745
|
-
|
|
41746
|
-
|
|
41747
|
-
|
|
41748
|
-
|
|
41749
|
-
|
|
41750
|
-
|
|
41751
|
-
|
|
41752
|
-
|
|
41753
|
-
|
|
41754
|
-
|
|
41755
|
-
|
|
41756
|
-
|
|
41757
|
-
|
|
41758
|
-
|
|
41759
|
-
|
|
41760
|
-
|
|
41761
|
-
|
|
41762
|
-
|
|
41763
|
-
filesWritten.push(filePath);
|
|
41764
|
-
} else {
|
|
41765
|
-
counters.skipped++;
|
|
41766
|
-
}
|
|
41767
|
-
}
|
|
41768
|
-
const fileKeys = Object.keys(def.files);
|
|
41769
|
-
const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
|
|
41770
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
41771
|
-
{ outputFile: fileKeys[0] }
|
|
41772
|
-
) : void 0);
|
|
41773
|
-
if (effectiveCombined && renderedFiles.size > 0) {
|
|
41774
|
-
const excluded = new Set(effectiveCombined.exclude ?? []);
|
|
41775
|
-
const parts = [];
|
|
41776
|
-
for (const filename of Object.keys(def.files)) {
|
|
41777
|
-
if (!excluded.has(filename) && renderedFiles.has(filename)) {
|
|
41778
|
-
parts.push(renderedFiles.get(filename) ?? "");
|
|
41779
|
-
}
|
|
41780
|
-
}
|
|
41781
|
-
if (parts.length > 0) {
|
|
41782
|
-
const combinedContent = parts.join("\n\n---\n\n");
|
|
41783
|
-
const combinedPath = join4(entityDir, effectiveCombined.outputFile);
|
|
41784
|
-
if (atomicWrite(combinedPath, combinedContent)) {
|
|
41785
|
-
filesWritten.push(combinedPath);
|
|
41770
|
+
const renderedFiles = /* @__PURE__ */ new Map();
|
|
41771
|
+
const entityFileHashes = {};
|
|
41772
|
+
const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
|
|
41773
|
+
for (const [filename, spec] of Object.entries(def.files)) {
|
|
41774
|
+
if (signal?.aborted) return null;
|
|
41775
|
+
const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
|
|
41776
|
+
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
41777
|
+
const rows = await resolveEntitySource(
|
|
41778
|
+
source,
|
|
41779
|
+
entityRow,
|
|
41780
|
+
entityPk,
|
|
41781
|
+
this._adapter,
|
|
41782
|
+
protection
|
|
41783
|
+
);
|
|
41784
|
+
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
41785
|
+
const renderFn = compileEntityRender(spec.render);
|
|
41786
|
+
const content = truncateContent(renderFn(rows), spec.budget);
|
|
41787
|
+
renderedFiles.set(filename, content);
|
|
41788
|
+
entityFileHashes[filename] = { hash: contentHash(content) };
|
|
41789
|
+
const filePath = join4(entityDir, filename);
|
|
41790
|
+
if (atomicWrite(filePath, content)) {
|
|
41791
|
+
filesWritten.push(filePath);
|
|
41786
41792
|
} else {
|
|
41787
41793
|
counters.skipped++;
|
|
41788
41794
|
}
|
|
41789
|
-
renderedFiles.set(effectiveCombined.outputFile, combinedContent);
|
|
41790
|
-
entityFileHashes[effectiveCombined.outputFile] = { hash: contentHash(combinedContent) };
|
|
41791
41795
|
}
|
|
41796
|
+
const fileKeys = Object.keys(def.files);
|
|
41797
|
+
const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
|
|
41798
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
41799
|
+
{ outputFile: fileKeys[0] }
|
|
41800
|
+
) : void 0);
|
|
41801
|
+
if (effectiveCombined && renderedFiles.size > 0) {
|
|
41802
|
+
const excluded = new Set(effectiveCombined.exclude ?? []);
|
|
41803
|
+
const parts = [];
|
|
41804
|
+
for (const filename of Object.keys(def.files)) {
|
|
41805
|
+
if (!excluded.has(filename) && renderedFiles.has(filename)) {
|
|
41806
|
+
parts.push(renderedFiles.get(filename) ?? "");
|
|
41807
|
+
}
|
|
41808
|
+
}
|
|
41809
|
+
if (parts.length > 0) {
|
|
41810
|
+
const combinedContent = parts.join("\n\n---\n\n");
|
|
41811
|
+
const combinedPath = join4(entityDir, effectiveCombined.outputFile);
|
|
41812
|
+
if (atomicWrite(combinedPath, combinedContent)) {
|
|
41813
|
+
filesWritten.push(combinedPath);
|
|
41814
|
+
} else {
|
|
41815
|
+
counters.skipped++;
|
|
41816
|
+
}
|
|
41817
|
+
renderedFiles.set(effectiveCombined.outputFile, combinedContent);
|
|
41818
|
+
entityFileHashes[effectiveCombined.outputFile] = {
|
|
41819
|
+
hash: contentHash(combinedContent)
|
|
41820
|
+
};
|
|
41821
|
+
}
|
|
41822
|
+
}
|
|
41823
|
+
manifestEntry.entities[slug] = entityFileHashes;
|
|
41824
|
+
const entitiesRendered = i6 + 1;
|
|
41825
|
+
throttle.tick({
|
|
41826
|
+
kind: "table-progress",
|
|
41827
|
+
table,
|
|
41828
|
+
entitiesRendered,
|
|
41829
|
+
entitiesTotal,
|
|
41830
|
+
tableIndex,
|
|
41831
|
+
tableCount,
|
|
41832
|
+
pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
|
|
41833
|
+
});
|
|
41792
41834
|
}
|
|
41793
|
-
|
|
41794
|
-
|
|
41795
|
-
throttle.tick({
|
|
41796
|
-
kind: "table-progress",
|
|
41835
|
+
throttle.force({
|
|
41836
|
+
kind: "table-done",
|
|
41797
41837
|
table,
|
|
41798
|
-
entitiesRendered,
|
|
41838
|
+
entitiesRendered: entitiesTotal,
|
|
41799
41839
|
entitiesTotal,
|
|
41800
41840
|
tableIndex,
|
|
41801
41841
|
tableCount,
|
|
41802
|
-
pct:
|
|
41842
|
+
pct: 100
|
|
41803
41843
|
});
|
|
41844
|
+
return manifestEntry;
|
|
41804
41845
|
}
|
|
41805
|
-
|
|
41806
|
-
|
|
41807
|
-
|
|
41808
|
-
|
|
41809
|
-
|
|
41810
|
-
|
|
41811
|
-
|
|
41812
|
-
tableCount,
|
|
41813
|
-
pct: 100
|
|
41814
|
-
});
|
|
41846
|
+
);
|
|
41847
|
+
if (signal?.aborted) return null;
|
|
41848
|
+
const manifestData = {};
|
|
41849
|
+
for (let i6 = 0; i6 < renderedEntries.length; i6++) {
|
|
41850
|
+
const entry = renderedEntries[i6];
|
|
41851
|
+
if (entry == null) return null;
|
|
41852
|
+
manifestData[entityTables[i6][0]] = entry;
|
|
41815
41853
|
}
|
|
41816
41854
|
return manifestData;
|
|
41817
41855
|
}
|
|
@@ -43071,14 +43109,27 @@ function buildParsedConfig(raw, sourceName, configDir2) {
|
|
|
43071
43109
|
const entityContexts = parseEntityContexts(config.entityContexts);
|
|
43072
43110
|
return name !== void 0 ? { dbPath, name, tables, entityContexts } : { dbPath, tables, entityContexts };
|
|
43073
43111
|
}
|
|
43112
|
+
function isDbRefShaped(raw) {
|
|
43113
|
+
return /^\s*\$\{LATTICE_DB:/.test(raw);
|
|
43114
|
+
}
|
|
43115
|
+
function parseDbRef(raw) {
|
|
43116
|
+
const m4 = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(raw.trim());
|
|
43117
|
+
return m4 ? { label: m4[1] ?? "" } : null;
|
|
43118
|
+
}
|
|
43074
43119
|
function resolveDbPath(raw, configDir2) {
|
|
43075
|
-
|
|
43076
|
-
|
|
43077
|
-
|
|
43078
|
-
|
|
43120
|
+
if (isDbRefShaped(raw)) {
|
|
43121
|
+
const ref = parseDbRef(raw);
|
|
43122
|
+
if (!ref) {
|
|
43123
|
+
throw new Error(
|
|
43124
|
+
`Lattice: malformed \${LATTICE_DB:\u2026} reference ${JSON.stringify(
|
|
43125
|
+
raw.trim()
|
|
43126
|
+
)} \u2014 the label may contain only [A-Za-z0-9._-] (no spaces). This usually means a workspace was created with an unsanitized name.`
|
|
43127
|
+
);
|
|
43128
|
+
}
|
|
43129
|
+
const url = getDbCredential(ref.label);
|
|
43079
43130
|
if (!url) {
|
|
43080
43131
|
throw new Error(
|
|
43081
|
-
`Lattice: config references \${LATTICE_DB:${label}} but no credential is saved for "${label}". Save one via the GUI's Database panel or set LATTICE_DB_${label}.`
|
|
43132
|
+
`Lattice: config references \${LATTICE_DB:${ref.label}} but no credential is saved for "${ref.label}". Save one via the GUI's Database panel or set LATTICE_DB_${ref.label}.`
|
|
43082
43133
|
);
|
|
43083
43134
|
}
|
|
43084
43135
|
return url;
|
|
@@ -43086,6 +43137,13 @@ function resolveDbPath(raw, configDir2) {
|
|
|
43086
43137
|
if (/^postgres(ql)?:\/\//i.test(raw) || raw.startsWith("file:") || raw === ":memory:") {
|
|
43087
43138
|
return raw;
|
|
43088
43139
|
}
|
|
43140
|
+
if (raw.includes("${")) {
|
|
43141
|
+
throw new Error(
|
|
43142
|
+
`Lattice: refusing to treat ${JSON.stringify(
|
|
43143
|
+
raw.trim()
|
|
43144
|
+
)} as a database path \u2014 it looks like a malformed variable reference, not a file path.`
|
|
43145
|
+
);
|
|
43146
|
+
}
|
|
43089
43147
|
return resolve2(configDir2, raw);
|
|
43090
43148
|
}
|
|
43091
43149
|
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
@@ -46534,6 +46592,10 @@ var NATIVE_ENTITY_NAMES = new Set(Object.keys(NATIVE_ENTITY_DEFS));
|
|
|
46534
46592
|
function isNativeEntity(name) {
|
|
46535
46593
|
return NATIVE_ENTITY_NAMES.has(name);
|
|
46536
46594
|
}
|
|
46595
|
+
var NATIVE_INTERNAL_NAMES = /* @__PURE__ */ new Set([
|
|
46596
|
+
"chat_threads",
|
|
46597
|
+
"chat_messages"
|
|
46598
|
+
]);
|
|
46537
46599
|
function registerNativeEntities(db) {
|
|
46538
46600
|
const existing = new Set(db.getRegisteredTableNames());
|
|
46539
46601
|
for (const [name, def] of Object.entries(NATIVE_ENTITY_DEFS)) {
|
|
@@ -47139,7 +47201,7 @@ function archiveLocalSqlite(dbPath) {
|
|
|
47139
47201
|
async function cloudRlsInstalled(probe) {
|
|
47140
47202
|
const row = await getAsyncOrSync(
|
|
47141
47203
|
probe.adapter,
|
|
47142
|
-
`SELECT to_regclass('
|
|
47204
|
+
`SELECT to_regclass('__lattice_owners') AS reg`
|
|
47143
47205
|
);
|
|
47144
47206
|
return !!row && row.reg != null;
|
|
47145
47207
|
}
|
|
@@ -47196,6 +47258,19 @@ function isPostgresUrl(url) {
|
|
|
47196
47258
|
}
|
|
47197
47259
|
|
|
47198
47260
|
// src/cloud/rls.ts
|
|
47261
|
+
async function runCloudBootstrapSql(db, sql) {
|
|
47262
|
+
const adapter = db.adapter;
|
|
47263
|
+
if (adapter.withClient) {
|
|
47264
|
+
await adapter.withClient(async (tx) => {
|
|
47265
|
+
await tx.run("SELECT pg_advisory_xact_lock($1::bigint)", [
|
|
47266
|
+
LATTICE_MIGRATION_LOCK_ID.toString()
|
|
47267
|
+
]);
|
|
47268
|
+
await tx.run(sql);
|
|
47269
|
+
});
|
|
47270
|
+
} else {
|
|
47271
|
+
await runAsyncOrSync(adapter, sql);
|
|
47272
|
+
}
|
|
47273
|
+
}
|
|
47199
47274
|
function isPg(db) {
|
|
47200
47275
|
return db.getDialect() === "postgres";
|
|
47201
47276
|
}
|
|
@@ -47329,12 +47404,42 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
|
|
|
47329
47404
|
"id" text PRIMARY KEY,
|
|
47330
47405
|
"role" text NOT NULL,
|
|
47331
47406
|
"email_hash" text NOT NULL,
|
|
47407
|
+
"email" text,
|
|
47332
47408
|
"created_by" text NOT NULL DEFAULT session_user,
|
|
47333
47409
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
47334
47410
|
"expires_at" timestamptz NOT NULL,
|
|
47335
47411
|
"redeemed_at" timestamptz,
|
|
47336
47412
|
"revoked_at" timestamptz
|
|
47337
47413
|
);
|
|
47414
|
+
-- Plaintext invitee email (owner-only table; members have no grant) so the
|
|
47415
|
+
-- owner's Members list can show who each member is. Added via ALTER so clouds
|
|
47416
|
+
-- created before this column converge to it on the owner's next open (the
|
|
47417
|
+
-- bootstrap is now run directly + idempotently, not version-gated).
|
|
47418
|
+
ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
|
|
47419
|
+
|
|
47420
|
+
-- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
|
|
47421
|
+
-- the cloud with their minted credential, the join path calls this to CLAIM the
|
|
47422
|
+
-- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
|
|
47423
|
+
-- an invite for the CALLING role (session_user) is still pending: not already
|
|
47424
|
+
-- redeemed (one-time-use), not revoked, and not expired. A replayed redeem of a
|
|
47425
|
+
-- leaked token, a revoked invite, or an expired one returns false, so the caller
|
|
47426
|
+
-- rejects the join. Members have no direct grant on the owner-only
|
|
47427
|
+
-- __lattice_member_invites table \u2014 this SECURITY DEFINER function is the only
|
|
47428
|
+
-- path, and it can claim ONLY the caller's own invite (keyed on session_user,
|
|
47429
|
+
-- never a caller-supplied parameter, so one member can't burn another's invite).
|
|
47430
|
+
CREATE OR REPLACE FUNCTION lattice_claim_invite()
|
|
47431
|
+
RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
47432
|
+
DECLARE v_ok boolean;
|
|
47433
|
+
BEGIN
|
|
47434
|
+
UPDATE "__lattice_member_invites"
|
|
47435
|
+
SET "redeemed_at" = now()
|
|
47436
|
+
WHERE "role" = session_user
|
|
47437
|
+
AND "redeemed_at" IS NULL
|
|
47438
|
+
AND "revoked_at" IS NULL
|
|
47439
|
+
AND "expires_at" > now()
|
|
47440
|
+
RETURNING true INTO v_ok;
|
|
47441
|
+
RETURN COALESCE(v_ok, false);
|
|
47442
|
+
END $fn$;
|
|
47338
47443
|
|
|
47339
47444
|
-- Visibility check. SECURITY DEFINER so it reads bookkeeping the member can't;
|
|
47340
47445
|
-- keyed on session_user (the member's login role). A row with no ownership record
|
|
@@ -47617,6 +47722,62 @@ END $fn$;
|
|
|
47617
47722
|
DROP TRIGGER IF EXISTS "lattice_notify_change_trg" ON "__lattice_changes";
|
|
47618
47723
|
CREATE TRIGGER "lattice_notify_change_trg" AFTER INSERT ON "__lattice_changes"
|
|
47619
47724
|
FOR EACH ROW EXECUTE FUNCTION lattice_notify_change();
|
|
47725
|
+
|
|
47726
|
+
-- #4.4 \u2014 seq-based catch-up after a realtime gap. NOTIFY is fire-and-forget, so a
|
|
47727
|
+
-- broker that drops its LISTEN (network blip, laptop sleep) misses every change
|
|
47728
|
+
-- during the gap. The broker tracks the highest seq it delivered and, on
|
|
47729
|
+
-- reconnect, replays what it missed via this function. Members have NO direct
|
|
47730
|
+
-- grant on __lattice_changes (reading it raw would leak every change on the
|
|
47731
|
+
-- cloud), so this SECURITY DEFINER function is the only path and it returns ONLY
|
|
47732
|
+
-- the rows the CALLING role can see: keyed on session_user via lattice_row_visible
|
|
47733
|
+
-- (same gate as live fan-out, #4.3). Deletes are excluded \u2014 the ownership record
|
|
47734
|
+
-- is gone post-delete so visibility can't be verified, and replaying them would
|
|
47735
|
+
-- leak deleted-row pks (the client reconciles deletes on its reconnect refetch).
|
|
47736
|
+
-- Bounded (LIMIT clamped \u2264 1000) so a long gap can't stream the whole table (Rule:
|
|
47737
|
+
-- bounded reads on a hot path).
|
|
47738
|
+
CREATE OR REPLACE FUNCTION lattice_changes_since(p_seq bigint, p_limit int)
|
|
47739
|
+
RETURNS TABLE(seq bigint, table_name text, pk text, op text, owner_role text, created_at timestamptz)
|
|
47740
|
+
LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
47741
|
+
SELECT c."seq", c."table_name", c."pk", c."op", c."owner_role", c."created_at"
|
|
47742
|
+
FROM "__lattice_changes" c
|
|
47743
|
+
WHERE c."seq" > p_seq
|
|
47744
|
+
AND c."op" = 'upsert'
|
|
47745
|
+
AND lattice_row_visible(c."table_name", c."pk")
|
|
47746
|
+
ORDER BY c."seq" ASC
|
|
47747
|
+
LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 500), 1000));
|
|
47748
|
+
$fn$;
|
|
47749
|
+
|
|
47750
|
+
-- #2.1 \u2014 per-row access summary for the connecting role. The GUI attaches this as
|
|
47751
|
+
-- each row's _access so the sharing affordance renders, but __lattice_owners is
|
|
47752
|
+
-- owner-only bookkeeping (members have no grant), so a member reading it directly
|
|
47753
|
+
-- got "permission denied". This SECURITY DEFINER function returns visibility +
|
|
47754
|
+
-- whether the CALLER owns the row, ONLY for the rows the caller can actually see
|
|
47755
|
+
-- (lattice_row_visible, keyed on session_user) \u2014 so a member learns nothing about
|
|
47756
|
+
-- rows hidden from it. Member-callable; the owner gets the same view of its rows.
|
|
47757
|
+
CREATE OR REPLACE FUNCTION lattice_rows_access(p_table text, p_pks text[])
|
|
47758
|
+
RETURNS TABLE(pk text, visibility text, owned boolean)
|
|
47759
|
+
LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
47760
|
+
SELECT o."pk", o."visibility", (o."owner_role" = session_user) AS owned
|
|
47761
|
+
FROM "__lattice_owners" o
|
|
47762
|
+
WHERE o."table_name" = p_table
|
|
47763
|
+
AND o."pk" = ANY(p_pks)
|
|
47764
|
+
AND lattice_row_visible(o."table_name", o."pk");
|
|
47765
|
+
$fn$;
|
|
47766
|
+
|
|
47767
|
+
-- #2.1 \u2014 grantees of a CALLER-OWNED custom-shared row (who you shared YOUR row
|
|
47768
|
+
-- with). Only the row owner sees this (the WHERE pins owner_role = session_user),
|
|
47769
|
+
-- so a member can't enumerate another owner's grants. __lattice_row_grants is
|
|
47770
|
+
-- member-ungranted, so this SECURITY DEFINER function is the member-safe path.
|
|
47771
|
+
CREATE OR REPLACE FUNCTION lattice_row_grantees(p_table text, p_pks text[])
|
|
47772
|
+
RETURNS TABLE(pk text, grantee_role text)
|
|
47773
|
+
LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
47774
|
+
SELECT g."pk", g."grantee_role"
|
|
47775
|
+
FROM "__lattice_row_grants" g
|
|
47776
|
+
JOIN "__lattice_owners" o ON o."table_name" = g."table_name" AND o."pk" = g."pk"
|
|
47777
|
+
WHERE g."table_name" = p_table
|
|
47778
|
+
AND g."pk" = ANY(p_pks)
|
|
47779
|
+
AND o."owner_role" = session_user;
|
|
47780
|
+
$fn$;
|
|
47620
47781
|
`;
|
|
47621
47782
|
function tableRlsSql(table, pkCols) {
|
|
47622
47783
|
const q3 = `"${table.replace(/"/g, '""')}"`;
|
|
@@ -47684,28 +47845,14 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
|
|
|
47684
47845
|
async function installCloudRls(db) {
|
|
47685
47846
|
if (!isPg(db)) return;
|
|
47686
47847
|
const schema = await cloudSchema(db);
|
|
47687
|
-
const
|
|
47688
|
-
|
|
47689
|
-
// model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell);
|
|
47690
|
-
// v6 added per-table policy (__lattice_table_policy: default_row_visibility +
|
|
47691
|
-
// never_share, enforced in the insert trigger + share/grant guards), the
|
|
47692
|
-
// canonical column-audience store (__lattice_column_policy), lattice_is_owner,
|
|
47693
|
-
// and the owner-only setters; v7 pins search_path on every SECURITY DEFINER
|
|
47694
|
-
// helper (closes the pg_temp-shadow RLS bypass) + revokes schema CREATE from
|
|
47695
|
-
// PUBLIC. The bootstrap is fully idempotent.
|
|
47696
|
-
version: "internal:cloud-rls:bootstrap:v7",
|
|
47697
|
-
sql: pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema)
|
|
47698
|
-
};
|
|
47699
|
-
await db.migrate([migration]);
|
|
47848
|
+
const sql = pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema);
|
|
47849
|
+
await runCloudBootstrapSql(db, sql);
|
|
47700
47850
|
}
|
|
47701
47851
|
async function enableChangelogRls(db) {
|
|
47702
47852
|
if (!isPg(db)) return;
|
|
47703
|
-
|
|
47704
|
-
|
|
47705
|
-
|
|
47706
|
-
// existing clouds.
|
|
47707
|
-
version: "internal:cloud-rls:changelog:v2",
|
|
47708
|
-
sql: `
|
|
47853
|
+
await runCloudBootstrapSql(
|
|
47854
|
+
db,
|
|
47855
|
+
`
|
|
47709
47856
|
ALTER TABLE "__lattice_changelog" ENABLE ROW LEVEL SECURITY;
|
|
47710
47857
|
ALTER TABLE "__lattice_changelog" FORCE ROW LEVEL SECURITY;
|
|
47711
47858
|
GRANT SELECT, INSERT ON "__lattice_changelog" TO ${MEMBER_GROUP};
|
|
@@ -47715,6 +47862,7 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
|
|
|
47715
47862
|
CASE
|
|
47716
47863
|
WHEN "change_kind" = 'derived' THEN
|
|
47717
47864
|
"source_ref" IS NOT NULL
|
|
47865
|
+
AND jsonb_array_length("source_ref"::jsonb) > 0
|
|
47718
47866
|
AND NOT EXISTS (
|
|
47719
47867
|
SELECT 1 FROM jsonb_array_elements_text("source_ref"::jsonb) AS src(sid)
|
|
47720
47868
|
WHERE NOT lattice_source_visible(src.sid)
|
|
@@ -47725,8 +47873,7 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
|
|
|
47725
47873
|
DROP POLICY IF EXISTS "lattice_changelog_ins" ON "__lattice_changelog";
|
|
47726
47874
|
CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH CHECK (true);
|
|
47727
47875
|
`
|
|
47728
|
-
|
|
47729
|
-
await db.migrate([migration]);
|
|
47876
|
+
);
|
|
47730
47877
|
}
|
|
47731
47878
|
async function enableRlsForTable(db, table, pkCols) {
|
|
47732
47879
|
if (!isPg(db)) return;
|
|
@@ -47780,7 +47927,12 @@ async function provisionMemberRole(db, role, password) {
|
|
|
47780
47927
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
|
|
47781
47928
|
CREATE ROLE "${role}" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '${password}';
|
|
47782
47929
|
ELSE
|
|
47783
|
-
|
|
47930
|
+
-- Re-invite of an EXISTING role: set ONLY what changed (login + password).
|
|
47931
|
+
-- Restating NOSUPERUSER/superuser-class attrs trips Supabase supautils
|
|
47932
|
+
-- ("only superuser may alter the SUPERUSER attribute", 42501) since the
|
|
47933
|
+
-- owner 'postgres' isn't a true superuser. The role was already created
|
|
47934
|
+
-- NOSUPERUSER NOCREATEDB NOCREATEROLE, so there is nothing to restate.
|
|
47935
|
+
ALTER ROLE "${role}" WITH LOGIN PASSWORD '${password}';
|
|
47784
47936
|
END IF;
|
|
47785
47937
|
END $LATTICE$`
|
|
47786
47938
|
);
|
|
@@ -47819,9 +47971,28 @@ async function revokeCell(db, table, pk, column, grantee) {
|
|
|
47819
47971
|
async function revokeMemberRole(db, role) {
|
|
47820
47972
|
assertPg(db);
|
|
47821
47973
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
47822
|
-
|
|
47974
|
+
const exists = await getAsyncOrSync(
|
|
47975
|
+
db.adapter,
|
|
47976
|
+
`SELECT 1 AS x FROM pg_roles WHERE rolname = ?`,
|
|
47977
|
+
[role]
|
|
47978
|
+
);
|
|
47979
|
+
if (!exists) return;
|
|
47980
|
+
for (const stmt of [`REASSIGN OWNED BY "${role}" TO CURRENT_USER`, `DROP OWNED BY "${role}"`]) {
|
|
47981
|
+
try {
|
|
47982
|
+
await runAsyncOrSync(db.adapter, stmt);
|
|
47983
|
+
} catch (e6) {
|
|
47984
|
+
if (!isInsufficientPrivilege(e6)) throw e6;
|
|
47985
|
+
console.warn(
|
|
47986
|
+
`[cloud] "${stmt.split(" ").slice(0, 2).join(" ")} \u2026" skipped (insufficient privilege; a scoped member owns no objects): ${e6.message}`
|
|
47987
|
+
);
|
|
47988
|
+
}
|
|
47989
|
+
}
|
|
47823
47990
|
await runAsyncOrSync(db.adapter, `DROP ROLE IF EXISTS "${role}"`);
|
|
47824
47991
|
}
|
|
47992
|
+
function isInsufficientPrivilege(e6) {
|
|
47993
|
+
const err = e6 ?? {};
|
|
47994
|
+
return err.code === "42501" || /permission denied/i.test(err.message ?? "");
|
|
47995
|
+
}
|
|
47825
47996
|
|
|
47826
47997
|
// src/cloud/discover.ts
|
|
47827
47998
|
async function discoverCloudTables(db) {
|
|
@@ -48065,6 +48236,7 @@ var FoldCache = class {
|
|
|
48065
48236
|
};
|
|
48066
48237
|
|
|
48067
48238
|
// src/cloud/settings.ts
|
|
48239
|
+
import { createHash as createHash8, randomBytes as randomBytes6 } from "crypto";
|
|
48068
48240
|
var CLOUD_SETTING_SYSTEM_PROMPT = "chat_system_prompt";
|
|
48069
48241
|
var CLOUD_SETTINGS_BOOTSTRAP_SQL = `
|
|
48070
48242
|
-- Owner-controlled, cloud-wide key/value settings. No grant to the member group,
|
|
@@ -48104,13 +48276,7 @@ END $fn$;
|
|
|
48104
48276
|
async function installCloudSettings(db) {
|
|
48105
48277
|
if (db.getDialect() !== "postgres") return;
|
|
48106
48278
|
const schema = await cloudSchema(db);
|
|
48107
|
-
|
|
48108
|
-
// v2 pins search_path on the two SECURITY DEFINER helpers (closes the
|
|
48109
|
-
// pg_temp-shadow class of bypass on the settings getter/setter).
|
|
48110
|
-
version: "internal:cloud-settings:v2",
|
|
48111
|
-
sql: pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema)
|
|
48112
|
-
};
|
|
48113
|
-
await db.migrate([migration]);
|
|
48279
|
+
await runCloudBootstrapSql(db, pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema));
|
|
48114
48280
|
}
|
|
48115
48281
|
async function getCloudSetting(db, key) {
|
|
48116
48282
|
if (db.getDialect() !== "postgres") return null;
|
|
@@ -48129,6 +48295,54 @@ async function setCloudSetting(db, key, value) {
|
|
|
48129
48295
|
}
|
|
48130
48296
|
|
|
48131
48297
|
// src/cloud/setup.ts
|
|
48298
|
+
var PRIVATE_ONLY_TABLES = [...NATIVE_INTERNAL_NAMES, "secrets"];
|
|
48299
|
+
async function reconcileCloudMemberAccess(db) {
|
|
48300
|
+
if (db.getDialect() !== "postgres") return;
|
|
48301
|
+
const registered = db.getRegisteredTableNames();
|
|
48302
|
+
for (const t8 of PRIVATE_ONLY_TABLES) {
|
|
48303
|
+
if (!registered.includes(t8)) continue;
|
|
48304
|
+
await runAsyncOrSync(
|
|
48305
|
+
db.adapter,
|
|
48306
|
+
`SELECT lattice_set_table_never_share('${t8.replace(/'/g, "''")}', true)`
|
|
48307
|
+
);
|
|
48308
|
+
}
|
|
48309
|
+
const rlsRows = await allAsyncOrSync(
|
|
48310
|
+
db.adapter,
|
|
48311
|
+
`SELECT c.relname AS name FROM pg_class c
|
|
48312
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48313
|
+
WHERE n.nspname = current_schema() AND c.relkind = 'r' AND c.relrowsecurity`
|
|
48314
|
+
);
|
|
48315
|
+
const rlsOn = new Set(rlsRows.map((r6) => r6.name));
|
|
48316
|
+
for (const table of registered) {
|
|
48317
|
+
if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
|
|
48318
|
+
if (!rlsOn.has(table)) continue;
|
|
48319
|
+
if (db.getPrimaryKey(table).length === 0) continue;
|
|
48320
|
+
const q3 = `"${table.replace(/"/g, '""')}"`;
|
|
48321
|
+
const masked = tableNeedsAudienceView(db.getColumnAudience(table) ?? {});
|
|
48322
|
+
if (masked) {
|
|
48323
|
+
const v2 = `"${`${table}_v`.replace(/"/g, '""')}"`;
|
|
48324
|
+
await runAsyncOrSync(db.adapter, `GRANT SELECT ON ${v2} TO ${MEMBER_GROUP}`);
|
|
48325
|
+
await runAsyncOrSync(db.adapter, `GRANT INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`);
|
|
48326
|
+
} else {
|
|
48327
|
+
await runAsyncOrSync(
|
|
48328
|
+
db.adapter,
|
|
48329
|
+
`GRANT SELECT, INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`
|
|
48330
|
+
);
|
|
48331
|
+
}
|
|
48332
|
+
}
|
|
48333
|
+
}
|
|
48334
|
+
async function secureNewCloudTable(db, table, pk) {
|
|
48335
|
+
if (db.getDialect() !== "postgres") return;
|
|
48336
|
+
if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
|
|
48337
|
+
if (pk.length === 0) return;
|
|
48338
|
+
await backfillOwnership(db, table, pk);
|
|
48339
|
+
await enableRlsForTable(db, table, pk);
|
|
48340
|
+
const cols = db.getRegisteredColumns(table);
|
|
48341
|
+
if (cols) {
|
|
48342
|
+
await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
|
|
48343
|
+
await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
48344
|
+
}
|
|
48345
|
+
}
|
|
48132
48346
|
async function secureCloud(db) {
|
|
48133
48347
|
if (db.getDialect() !== "postgres") return;
|
|
48134
48348
|
await installCloudRls(db);
|
|
@@ -48137,20 +48351,9 @@ async function secureCloud(db) {
|
|
|
48137
48351
|
await enableChangelogRls(db);
|
|
48138
48352
|
const registered = db.getRegisteredTableNames();
|
|
48139
48353
|
for (const table of registered) {
|
|
48140
|
-
|
|
48141
|
-
const pk = db.getPrimaryKey(table);
|
|
48142
|
-
if (pk.length === 0) continue;
|
|
48143
|
-
await backfillOwnership(db, table, pk);
|
|
48144
|
-
await enableRlsForTable(db, table, pk);
|
|
48145
|
-
const cols = db.getRegisteredColumns(table);
|
|
48146
|
-
if (cols) {
|
|
48147
|
-
await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
|
|
48148
|
-
await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
48149
|
-
}
|
|
48150
|
-
}
|
|
48151
|
-
if (registered.includes("secrets")) {
|
|
48152
|
-
await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
|
|
48354
|
+
await secureNewCloudTable(db, table, db.getPrimaryKey(table));
|
|
48153
48355
|
}
|
|
48356
|
+
await reconcileCloudMemberAccess(db);
|
|
48154
48357
|
await runAsyncOrSync(
|
|
48155
48358
|
db.adapter,
|
|
48156
48359
|
`DO $LATTICE$ BEGIN
|