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.cjs
CHANGED
|
@@ -41629,37 +41629,63 @@ var THROTTLE_WINDOW_MS = 200;
|
|
|
41629
41629
|
var ProgressThrottle = class {
|
|
41630
41630
|
cb;
|
|
41631
41631
|
windowMs;
|
|
41632
|
-
|
|
41632
|
+
/**
|
|
41633
|
+
* Last passthrough time, keyed per table (`event.table`, or `''` for the
|
|
41634
|
+
* table-less `done`/`error` lifecycle events). Per-table — not a single shared
|
|
41635
|
+
* clock — so when tables render CONCURRENTLY each one keeps its own ~5/sec
|
|
41636
|
+
* budget: a fast table can't consume the window and starve a slow table's
|
|
41637
|
+
* progress. `force` (table-start) resets only that table's budget.
|
|
41638
|
+
*/
|
|
41639
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
41633
41640
|
constructor(cb, windowMs = THROTTLE_WINDOW_MS) {
|
|
41634
41641
|
this.cb = cb;
|
|
41635
41642
|
this.windowMs = windowMs;
|
|
41636
41643
|
}
|
|
41637
41644
|
/**
|
|
41638
|
-
* Emit a `table-progress` event, but only if the window since
|
|
41639
|
-
* passthrough has elapsed. Dropped events are simply not delivered — the
|
|
41640
|
-
* one that survives carries the latest running count.
|
|
41645
|
+
* Emit a `table-progress` event, but only if the window since this table's
|
|
41646
|
+
* last passthrough has elapsed. Dropped events are simply not delivered — the
|
|
41647
|
+
* next one that survives carries the latest running count.
|
|
41641
41648
|
*/
|
|
41642
41649
|
tick(event) {
|
|
41643
41650
|
if (!this.cb) return;
|
|
41651
|
+
const key = event.table ?? "";
|
|
41644
41652
|
const now = Date.now();
|
|
41645
|
-
if (now - this.lastEmit < this.windowMs) return;
|
|
41646
|
-
this.lastEmit
|
|
41653
|
+
if (now - (this.lastEmit.get(key) ?? 0) < this.windowMs) return;
|
|
41654
|
+
this.lastEmit.set(key, now);
|
|
41647
41655
|
this.cb(event);
|
|
41648
41656
|
}
|
|
41649
41657
|
/**
|
|
41650
|
-
* Emit a lifecycle event immediately and reset
|
|
41651
|
-
* `table-start`, `table-done`, `done`, and `error` — none of which
|
|
41652
|
-
* ever be dropped. Resetting on `table-start` gives each table a clean
|
|
41658
|
+
* Emit a lifecycle event immediately and reset this table's throttle window.
|
|
41659
|
+
* Use for `table-start`, `table-done`, `done`, and `error` — none of which
|
|
41660
|
+
* should ever be dropped. Resetting on `table-start` gives each table a clean
|
|
41661
|
+
* budget.
|
|
41653
41662
|
*/
|
|
41654
41663
|
force(event) {
|
|
41655
41664
|
if (!this.cb) return;
|
|
41656
|
-
this.lastEmit
|
|
41665
|
+
this.lastEmit.set(event.table ?? "", Date.now());
|
|
41657
41666
|
this.cb(event);
|
|
41658
41667
|
}
|
|
41659
41668
|
};
|
|
41660
41669
|
|
|
41670
|
+
// src/concurrency.ts
|
|
41671
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
41672
|
+
const results = new Array(items.length);
|
|
41673
|
+
let next = 0;
|
|
41674
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
41675
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
41676
|
+
for (; ; ) {
|
|
41677
|
+
const i6 = next++;
|
|
41678
|
+
if (i6 >= items.length) break;
|
|
41679
|
+
results[i6] = await fn(items[i6], i6);
|
|
41680
|
+
}
|
|
41681
|
+
});
|
|
41682
|
+
await Promise.all(workers);
|
|
41683
|
+
return results;
|
|
41684
|
+
}
|
|
41685
|
+
|
|
41661
41686
|
// src/render/engine.ts
|
|
41662
41687
|
var YIELD_EVERY_ENTITIES = 200;
|
|
41688
|
+
var RENDER_TABLE_CONCURRENCY = 4;
|
|
41663
41689
|
var NOOP_RENDER = () => "";
|
|
41664
41690
|
var RenderEngine = class {
|
|
41665
41691
|
_schema;
|
|
@@ -41841,163 +41867,175 @@ var RenderEngine = class {
|
|
|
41841
41867
|
* via `signal`.
|
|
41842
41868
|
*/
|
|
41843
41869
|
async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
|
|
41844
|
-
const manifestData = {};
|
|
41845
41870
|
const protectedTables = /* @__PURE__ */ new Set();
|
|
41846
41871
|
for (const [t8, d6] of this._schema.getEntityContexts()) {
|
|
41847
41872
|
if (d6.protected) protectedTables.add(t8);
|
|
41848
41873
|
}
|
|
41849
41874
|
const entityTables = [...this._schema.getEntityContexts()];
|
|
41850
41875
|
const tableCount = entityTables.length;
|
|
41851
|
-
|
|
41852
|
-
|
|
41853
|
-
|
|
41854
|
-
|
|
41855
|
-
|
|
41856
|
-
const directoryRoot = def.directoryRoot ?? table;
|
|
41857
|
-
const entitiesTotal = allRows.length;
|
|
41858
|
-
throttle.force({
|
|
41859
|
-
kind: "table-start",
|
|
41860
|
-
table,
|
|
41861
|
-
entitiesRendered: 0,
|
|
41862
|
-
entitiesTotal,
|
|
41863
|
-
tableIndex,
|
|
41864
|
-
tableCount,
|
|
41865
|
-
pct: 0
|
|
41866
|
-
});
|
|
41867
|
-
const manifestEntry = {
|
|
41868
|
-
directoryRoot,
|
|
41869
|
-
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
41870
|
-
declaredFiles: Object.keys(def.files),
|
|
41871
|
-
protectedFiles: def.protectedFiles ?? [],
|
|
41872
|
-
entities: {}
|
|
41873
|
-
};
|
|
41874
|
-
if (def.index) {
|
|
41875
|
-
const indexPath = (0, import_node_path5.join)(outputDir, def.index.outputFile);
|
|
41876
|
-
if (atomicWrite(indexPath, def.index.render(allRows))) {
|
|
41877
|
-
filesWritten.push(indexPath);
|
|
41878
|
-
} else {
|
|
41879
|
-
counters.skipped++;
|
|
41880
|
-
}
|
|
41881
|
-
}
|
|
41882
|
-
for (let i6 = 0; i6 < allRows.length; i6++) {
|
|
41883
|
-
const entityRow = allRows[i6];
|
|
41876
|
+
if (signal?.aborted) return null;
|
|
41877
|
+
const renderedEntries = await mapWithConcurrency(
|
|
41878
|
+
entityTables,
|
|
41879
|
+
RENDER_TABLE_CONCURRENCY,
|
|
41880
|
+
async ([table, def], tableIndex) => {
|
|
41884
41881
|
if (signal?.aborted) return null;
|
|
41885
|
-
|
|
41886
|
-
|
|
41887
|
-
|
|
41888
|
-
const
|
|
41889
|
-
|
|
41890
|
-
|
|
41891
|
-
|
|
41892
|
-
|
|
41893
|
-
|
|
41894
|
-
|
|
41895
|
-
|
|
41896
|
-
|
|
41897
|
-
|
|
41898
|
-
|
|
41899
|
-
|
|
41900
|
-
|
|
41901
|
-
|
|
41902
|
-
|
|
41903
|
-
|
|
41904
|
-
|
|
41905
|
-
|
|
41906
|
-
|
|
41882
|
+
const entityPk = this._schema.getPrimaryKey(table)[0] ?? "id";
|
|
41883
|
+
const allRows = await this._schema.queryTable(this._adapter, table);
|
|
41884
|
+
const directoryRoot = def.directoryRoot ?? table;
|
|
41885
|
+
const entitiesTotal = allRows.length;
|
|
41886
|
+
throttle.force({
|
|
41887
|
+
kind: "table-start",
|
|
41888
|
+
table,
|
|
41889
|
+
entitiesRendered: 0,
|
|
41890
|
+
entitiesTotal,
|
|
41891
|
+
tableIndex,
|
|
41892
|
+
tableCount,
|
|
41893
|
+
pct: 0
|
|
41894
|
+
});
|
|
41895
|
+
const manifestEntry = {
|
|
41896
|
+
directoryRoot,
|
|
41897
|
+
...def.index ? { indexFile: def.index.outputFile } : {},
|
|
41898
|
+
declaredFiles: Object.keys(def.files),
|
|
41899
|
+
protectedFiles: def.protectedFiles ?? [],
|
|
41900
|
+
entities: {}
|
|
41901
|
+
};
|
|
41902
|
+
if (def.index) {
|
|
41903
|
+
const indexPath = (0, import_node_path5.join)(outputDir, def.index.outputFile);
|
|
41904
|
+
if (atomicWrite(indexPath, def.index.render(allRows))) {
|
|
41905
|
+
filesWritten.push(indexPath);
|
|
41906
|
+
} else {
|
|
41907
|
+
counters.skipped++;
|
|
41908
|
+
}
|
|
41909
|
+
}
|
|
41910
|
+
for (let i6 = 0; i6 < allRows.length; i6++) {
|
|
41911
|
+
const entityRow = allRows[i6];
|
|
41912
|
+
if (signal?.aborted) return null;
|
|
41913
|
+
if (i6 > 0 && i6 % YIELD_EVERY_ENTITIES === 0) {
|
|
41914
|
+
await new Promise((r6) => setImmediate(r6));
|
|
41915
|
+
}
|
|
41916
|
+
const rawSlug = def.slug(entityRow);
|
|
41917
|
+
const slug = rawSlug.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ").replace(/[\u0000-\u001F\u007F]/g, "");
|
|
41918
|
+
if (/[^a-zA-Z0-9.\-_ @(),#&'+:;!~[\]]/.test(slug)) {
|
|
41919
|
+
throw new Error(`Invalid slug "${slug}": contains characters outside the allowed set`);
|
|
41920
|
+
}
|
|
41921
|
+
const entityDir = def.directory ? (0, import_node_path5.join)(outputDir, def.directory(entityRow)) : (0, import_node_path5.join)(outputDir, directoryRoot, slug);
|
|
41922
|
+
const resolvedDir = (0, import_node_path5.resolve)(entityDir);
|
|
41923
|
+
const resolvedBase = (0, import_node_path5.resolve)(outputDir);
|
|
41924
|
+
if (!resolvedDir.startsWith(resolvedBase + import_node_path5.sep) && resolvedDir !== resolvedBase) {
|
|
41925
|
+
throw new Error(`Path traversal detected: slug "${slug}" escapes output directory`);
|
|
41926
|
+
}
|
|
41927
|
+
(0, import_node_fs4.mkdirSync)(entityDir, { recursive: true });
|
|
41928
|
+
if (def.attachFileColumn) {
|
|
41929
|
+
const filePath = entityRow[def.attachFileColumn];
|
|
41930
|
+
if (filePath && typeof filePath === "string" && filePath.length > 0) {
|
|
41931
|
+
if (def.attachFileMode === "reference") {
|
|
41932
|
+
const refPath = (0, import_node_path5.join)(entityDir, `${(0, import_node_path5.basename)(filePath)}.ref.md`);
|
|
41933
|
+
try {
|
|
41934
|
+
atomicWrite(refPath, `# Reference
|
|
41907
41935
|
|
|
41908
41936
|
- **location:** ${filePath}
|
|
41909
41937
|
`);
|
|
41910
|
-
|
|
41911
|
-
|
|
41912
|
-
|
|
41913
|
-
|
|
41914
|
-
|
|
41915
|
-
|
|
41916
|
-
|
|
41917
|
-
|
|
41918
|
-
|
|
41919
|
-
|
|
41920
|
-
|
|
41921
|
-
|
|
41938
|
+
filesWritten.push(refPath);
|
|
41939
|
+
} catch {
|
|
41940
|
+
}
|
|
41941
|
+
} else {
|
|
41942
|
+
const absPath = (0, import_node_path5.isAbsolute)(filePath) ? filePath : (0, import_node_path5.resolve)(outputDir, filePath);
|
|
41943
|
+
if ((0, import_node_fs4.existsSync)(absPath)) {
|
|
41944
|
+
const destPath = (0, import_node_path5.join)(entityDir, (0, import_node_path5.basename)(absPath));
|
|
41945
|
+
if (!(0, import_node_fs4.existsSync)(destPath)) {
|
|
41946
|
+
try {
|
|
41947
|
+
(0, import_node_fs4.copyFileSync)(absPath, destPath);
|
|
41948
|
+
filesWritten.push(destPath);
|
|
41949
|
+
} catch {
|
|
41950
|
+
}
|
|
41922
41951
|
}
|
|
41923
41952
|
}
|
|
41924
41953
|
}
|
|
41925
41954
|
}
|
|
41926
41955
|
}
|
|
41927
|
-
|
|
41928
|
-
|
|
41929
|
-
|
|
41930
|
-
|
|
41931
|
-
|
|
41932
|
-
|
|
41933
|
-
|
|
41934
|
-
|
|
41935
|
-
|
|
41936
|
-
|
|
41937
|
-
|
|
41938
|
-
|
|
41939
|
-
|
|
41940
|
-
|
|
41941
|
-
|
|
41942
|
-
|
|
41943
|
-
|
|
41944
|
-
|
|
41945
|
-
|
|
41946
|
-
|
|
41947
|
-
|
|
41948
|
-
|
|
41949
|
-
filesWritten.push(filePath);
|
|
41950
|
-
} else {
|
|
41951
|
-
counters.skipped++;
|
|
41952
|
-
}
|
|
41953
|
-
}
|
|
41954
|
-
const fileKeys = Object.keys(def.files);
|
|
41955
|
-
const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
|
|
41956
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
41957
|
-
{ outputFile: fileKeys[0] }
|
|
41958
|
-
) : void 0);
|
|
41959
|
-
if (effectiveCombined && renderedFiles.size > 0) {
|
|
41960
|
-
const excluded = new Set(effectiveCombined.exclude ?? []);
|
|
41961
|
-
const parts = [];
|
|
41962
|
-
for (const filename of Object.keys(def.files)) {
|
|
41963
|
-
if (!excluded.has(filename) && renderedFiles.has(filename)) {
|
|
41964
|
-
parts.push(renderedFiles.get(filename) ?? "");
|
|
41965
|
-
}
|
|
41966
|
-
}
|
|
41967
|
-
if (parts.length > 0) {
|
|
41968
|
-
const combinedContent = parts.join("\n\n---\n\n");
|
|
41969
|
-
const combinedPath = (0, import_node_path5.join)(entityDir, effectiveCombined.outputFile);
|
|
41970
|
-
if (atomicWrite(combinedPath, combinedContent)) {
|
|
41971
|
-
filesWritten.push(combinedPath);
|
|
41956
|
+
const renderedFiles = /* @__PURE__ */ new Map();
|
|
41957
|
+
const entityFileHashes = {};
|
|
41958
|
+
const protection = protectedTables.size > 0 ? { protectedTables, currentTable: table } : void 0;
|
|
41959
|
+
for (const [filename, spec] of Object.entries(def.files)) {
|
|
41960
|
+
if (signal?.aborted) return null;
|
|
41961
|
+
const mergeDefaults = def.sourceDefaults && spec.source.type !== "self" && spec.source.type !== "custom" && spec.source.type !== "enriched";
|
|
41962
|
+
const source = mergeDefaults ? { ...def.sourceDefaults, ...spec.source } : spec.source;
|
|
41963
|
+
const rows = await resolveEntitySource(
|
|
41964
|
+
source,
|
|
41965
|
+
entityRow,
|
|
41966
|
+
entityPk,
|
|
41967
|
+
this._adapter,
|
|
41968
|
+
protection
|
|
41969
|
+
);
|
|
41970
|
+
if (spec.omitIfEmpty && rows.length === 0) continue;
|
|
41971
|
+
const renderFn = compileEntityRender(spec.render);
|
|
41972
|
+
const content = truncateContent(renderFn(rows), spec.budget);
|
|
41973
|
+
renderedFiles.set(filename, content);
|
|
41974
|
+
entityFileHashes[filename] = { hash: contentHash(content) };
|
|
41975
|
+
const filePath = (0, import_node_path5.join)(entityDir, filename);
|
|
41976
|
+
if (atomicWrite(filePath, content)) {
|
|
41977
|
+
filesWritten.push(filePath);
|
|
41972
41978
|
} else {
|
|
41973
41979
|
counters.skipped++;
|
|
41974
41980
|
}
|
|
41975
|
-
renderedFiles.set(effectiveCombined.outputFile, combinedContent);
|
|
41976
|
-
entityFileHashes[effectiveCombined.outputFile] = { hash: contentHash(combinedContent) };
|
|
41977
41981
|
}
|
|
41982
|
+
const fileKeys = Object.keys(def.files);
|
|
41983
|
+
const effectiveCombined = def.combined ?? (fileKeys.length > 1 && renderedFiles.size > 1 ? (
|
|
41984
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
41985
|
+
{ outputFile: fileKeys[0] }
|
|
41986
|
+
) : void 0);
|
|
41987
|
+
if (effectiveCombined && renderedFiles.size > 0) {
|
|
41988
|
+
const excluded = new Set(effectiveCombined.exclude ?? []);
|
|
41989
|
+
const parts = [];
|
|
41990
|
+
for (const filename of Object.keys(def.files)) {
|
|
41991
|
+
if (!excluded.has(filename) && renderedFiles.has(filename)) {
|
|
41992
|
+
parts.push(renderedFiles.get(filename) ?? "");
|
|
41993
|
+
}
|
|
41994
|
+
}
|
|
41995
|
+
if (parts.length > 0) {
|
|
41996
|
+
const combinedContent = parts.join("\n\n---\n\n");
|
|
41997
|
+
const combinedPath = (0, import_node_path5.join)(entityDir, effectiveCombined.outputFile);
|
|
41998
|
+
if (atomicWrite(combinedPath, combinedContent)) {
|
|
41999
|
+
filesWritten.push(combinedPath);
|
|
42000
|
+
} else {
|
|
42001
|
+
counters.skipped++;
|
|
42002
|
+
}
|
|
42003
|
+
renderedFiles.set(effectiveCombined.outputFile, combinedContent);
|
|
42004
|
+
entityFileHashes[effectiveCombined.outputFile] = {
|
|
42005
|
+
hash: contentHash(combinedContent)
|
|
42006
|
+
};
|
|
42007
|
+
}
|
|
42008
|
+
}
|
|
42009
|
+
manifestEntry.entities[slug] = entityFileHashes;
|
|
42010
|
+
const entitiesRendered = i6 + 1;
|
|
42011
|
+
throttle.tick({
|
|
42012
|
+
kind: "table-progress",
|
|
42013
|
+
table,
|
|
42014
|
+
entitiesRendered,
|
|
42015
|
+
entitiesTotal,
|
|
42016
|
+
tableIndex,
|
|
42017
|
+
tableCount,
|
|
42018
|
+
pct: entitiesTotal > 0 ? entitiesRendered / entitiesTotal * 100 : 100
|
|
42019
|
+
});
|
|
41978
42020
|
}
|
|
41979
|
-
|
|
41980
|
-
|
|
41981
|
-
throttle.tick({
|
|
41982
|
-
kind: "table-progress",
|
|
42021
|
+
throttle.force({
|
|
42022
|
+
kind: "table-done",
|
|
41983
42023
|
table,
|
|
41984
|
-
entitiesRendered,
|
|
42024
|
+
entitiesRendered: entitiesTotal,
|
|
41985
42025
|
entitiesTotal,
|
|
41986
42026
|
tableIndex,
|
|
41987
42027
|
tableCount,
|
|
41988
|
-
pct:
|
|
42028
|
+
pct: 100
|
|
41989
42029
|
});
|
|
42030
|
+
return manifestEntry;
|
|
41990
42031
|
}
|
|
41991
|
-
|
|
41992
|
-
|
|
41993
|
-
|
|
41994
|
-
|
|
41995
|
-
|
|
41996
|
-
|
|
41997
|
-
|
|
41998
|
-
tableCount,
|
|
41999
|
-
pct: 100
|
|
42000
|
-
});
|
|
42032
|
+
);
|
|
42033
|
+
if (signal?.aborted) return null;
|
|
42034
|
+
const manifestData = {};
|
|
42035
|
+
for (let i6 = 0; i6 < renderedEntries.length; i6++) {
|
|
42036
|
+
const entry = renderedEntries[i6];
|
|
42037
|
+
if (entry == null) return null;
|
|
42038
|
+
manifestData[entityTables[i6][0]] = entry;
|
|
42001
42039
|
}
|
|
42002
42040
|
return manifestData;
|
|
42003
42041
|
}
|
|
@@ -43249,14 +43287,27 @@ function buildParsedConfig(raw, sourceName, configDir2) {
|
|
|
43249
43287
|
const entityContexts = parseEntityContexts(config.entityContexts);
|
|
43250
43288
|
return name !== void 0 ? { dbPath, name, tables, entityContexts } : { dbPath, tables, entityContexts };
|
|
43251
43289
|
}
|
|
43290
|
+
function isDbRefShaped(raw) {
|
|
43291
|
+
return /^\s*\$\{LATTICE_DB:/.test(raw);
|
|
43292
|
+
}
|
|
43293
|
+
function parseDbRef(raw) {
|
|
43294
|
+
const m4 = /^\$\{LATTICE_DB:([A-Za-z0-9._-]+)\}$/.exec(raw.trim());
|
|
43295
|
+
return m4 ? { label: m4[1] ?? "" } : null;
|
|
43296
|
+
}
|
|
43252
43297
|
function resolveDbPath(raw, configDir2) {
|
|
43253
|
-
|
|
43254
|
-
|
|
43255
|
-
|
|
43256
|
-
|
|
43298
|
+
if (isDbRefShaped(raw)) {
|
|
43299
|
+
const ref = parseDbRef(raw);
|
|
43300
|
+
if (!ref) {
|
|
43301
|
+
throw new Error(
|
|
43302
|
+
`Lattice: malformed \${LATTICE_DB:\u2026} reference ${JSON.stringify(
|
|
43303
|
+
raw.trim()
|
|
43304
|
+
)} \u2014 the label may contain only [A-Za-z0-9._-] (no spaces). This usually means a workspace was created with an unsanitized name.`
|
|
43305
|
+
);
|
|
43306
|
+
}
|
|
43307
|
+
const url = getDbCredential(ref.label);
|
|
43257
43308
|
if (!url) {
|
|
43258
43309
|
throw new Error(
|
|
43259
|
-
`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}.`
|
|
43310
|
+
`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}.`
|
|
43260
43311
|
);
|
|
43261
43312
|
}
|
|
43262
43313
|
return url;
|
|
@@ -43264,6 +43315,13 @@ function resolveDbPath(raw, configDir2) {
|
|
|
43264
43315
|
if (/^postgres(ql)?:\/\//i.test(raw) || raw.startsWith("file:") || raw === ":memory:") {
|
|
43265
43316
|
return raw;
|
|
43266
43317
|
}
|
|
43318
|
+
if (raw.includes("${")) {
|
|
43319
|
+
throw new Error(
|
|
43320
|
+
`Lattice: refusing to treat ${JSON.stringify(
|
|
43321
|
+
raw.trim()
|
|
43322
|
+
)} as a database path \u2014 it looks like a malformed variable reference, not a file path.`
|
|
43323
|
+
);
|
|
43324
|
+
}
|
|
43267
43325
|
return (0, import_node_path11.resolve)(configDir2, raw);
|
|
43268
43326
|
}
|
|
43269
43327
|
var warnedDeprecatedRefs = /* @__PURE__ */ new Set();
|
|
@@ -46712,6 +46770,10 @@ var NATIVE_ENTITY_NAMES = new Set(Object.keys(NATIVE_ENTITY_DEFS));
|
|
|
46712
46770
|
function isNativeEntity(name) {
|
|
46713
46771
|
return NATIVE_ENTITY_NAMES.has(name);
|
|
46714
46772
|
}
|
|
46773
|
+
var NATIVE_INTERNAL_NAMES = /* @__PURE__ */ new Set([
|
|
46774
|
+
"chat_threads",
|
|
46775
|
+
"chat_messages"
|
|
46776
|
+
]);
|
|
46715
46777
|
function registerNativeEntities(db) {
|
|
46716
46778
|
const existing = new Set(db.getRegisteredTableNames());
|
|
46717
46779
|
for (const [name, def] of Object.entries(NATIVE_ENTITY_DEFS)) {
|
|
@@ -47317,7 +47379,7 @@ function archiveLocalSqlite(dbPath) {
|
|
|
47317
47379
|
async function cloudRlsInstalled(probe) {
|
|
47318
47380
|
const row = await getAsyncOrSync(
|
|
47319
47381
|
probe.adapter,
|
|
47320
|
-
`SELECT to_regclass('
|
|
47382
|
+
`SELECT to_regclass('__lattice_owners') AS reg`
|
|
47321
47383
|
);
|
|
47322
47384
|
return !!row && row.reg != null;
|
|
47323
47385
|
}
|
|
@@ -47374,6 +47436,19 @@ function isPostgresUrl(url) {
|
|
|
47374
47436
|
}
|
|
47375
47437
|
|
|
47376
47438
|
// src/cloud/rls.ts
|
|
47439
|
+
async function runCloudBootstrapSql(db, sql) {
|
|
47440
|
+
const adapter = db.adapter;
|
|
47441
|
+
if (adapter.withClient) {
|
|
47442
|
+
await adapter.withClient(async (tx) => {
|
|
47443
|
+
await tx.run("SELECT pg_advisory_xact_lock($1::bigint)", [
|
|
47444
|
+
LATTICE_MIGRATION_LOCK_ID.toString()
|
|
47445
|
+
]);
|
|
47446
|
+
await tx.run(sql);
|
|
47447
|
+
});
|
|
47448
|
+
} else {
|
|
47449
|
+
await runAsyncOrSync(adapter, sql);
|
|
47450
|
+
}
|
|
47451
|
+
}
|
|
47377
47452
|
function isPg(db) {
|
|
47378
47453
|
return db.getDialect() === "postgres";
|
|
47379
47454
|
}
|
|
@@ -47507,12 +47582,42 @@ CREATE TABLE IF NOT EXISTS "__lattice_member_invites" (
|
|
|
47507
47582
|
"id" text PRIMARY KEY,
|
|
47508
47583
|
"role" text NOT NULL,
|
|
47509
47584
|
"email_hash" text NOT NULL,
|
|
47585
|
+
"email" text,
|
|
47510
47586
|
"created_by" text NOT NULL DEFAULT session_user,
|
|
47511
47587
|
"created_at" timestamptz NOT NULL DEFAULT now(),
|
|
47512
47588
|
"expires_at" timestamptz NOT NULL,
|
|
47513
47589
|
"redeemed_at" timestamptz,
|
|
47514
47590
|
"revoked_at" timestamptz
|
|
47515
47591
|
);
|
|
47592
|
+
-- Plaintext invitee email (owner-only table; members have no grant) so the
|
|
47593
|
+
-- owner's Members list can show who each member is. Added via ALTER so clouds
|
|
47594
|
+
-- created before this column converge to it on the owner's next open (the
|
|
47595
|
+
-- bootstrap is now run directly + idempotently, not version-gated).
|
|
47596
|
+
ALTER TABLE "__lattice_member_invites" ADD COLUMN IF NOT EXISTS "email" text;
|
|
47597
|
+
|
|
47598
|
+
-- #3.1 \u2014 one-time-use + revocation enforcement. After a member authenticates to
|
|
47599
|
+
-- the cloud with their minted credential, the join path calls this to CLAIM the
|
|
47600
|
+
-- invite. The single atomic UPDATE stamps redeemed_at and returns true ONLY when
|
|
47601
|
+
-- an invite for the CALLING role (session_user) is still pending: not already
|
|
47602
|
+
-- redeemed (one-time-use), not revoked, and not expired. A replayed redeem of a
|
|
47603
|
+
-- leaked token, a revoked invite, or an expired one returns false, so the caller
|
|
47604
|
+
-- rejects the join. Members have no direct grant on the owner-only
|
|
47605
|
+
-- __lattice_member_invites table \u2014 this SECURITY DEFINER function is the only
|
|
47606
|
+
-- path, and it can claim ONLY the caller's own invite (keyed on session_user,
|
|
47607
|
+
-- never a caller-supplied parameter, so one member can't burn another's invite).
|
|
47608
|
+
CREATE OR REPLACE FUNCTION lattice_claim_invite()
|
|
47609
|
+
RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER AS $fn$
|
|
47610
|
+
DECLARE v_ok boolean;
|
|
47611
|
+
BEGIN
|
|
47612
|
+
UPDATE "__lattice_member_invites"
|
|
47613
|
+
SET "redeemed_at" = now()
|
|
47614
|
+
WHERE "role" = session_user
|
|
47615
|
+
AND "redeemed_at" IS NULL
|
|
47616
|
+
AND "revoked_at" IS NULL
|
|
47617
|
+
AND "expires_at" > now()
|
|
47618
|
+
RETURNING true INTO v_ok;
|
|
47619
|
+
RETURN COALESCE(v_ok, false);
|
|
47620
|
+
END $fn$;
|
|
47516
47621
|
|
|
47517
47622
|
-- Visibility check. SECURITY DEFINER so it reads bookkeeping the member can't;
|
|
47518
47623
|
-- keyed on session_user (the member's login role). A row with no ownership record
|
|
@@ -47795,6 +47900,62 @@ END $fn$;
|
|
|
47795
47900
|
DROP TRIGGER IF EXISTS "lattice_notify_change_trg" ON "__lattice_changes";
|
|
47796
47901
|
CREATE TRIGGER "lattice_notify_change_trg" AFTER INSERT ON "__lattice_changes"
|
|
47797
47902
|
FOR EACH ROW EXECUTE FUNCTION lattice_notify_change();
|
|
47903
|
+
|
|
47904
|
+
-- #4.4 \u2014 seq-based catch-up after a realtime gap. NOTIFY is fire-and-forget, so a
|
|
47905
|
+
-- broker that drops its LISTEN (network blip, laptop sleep) misses every change
|
|
47906
|
+
-- during the gap. The broker tracks the highest seq it delivered and, on
|
|
47907
|
+
-- reconnect, replays what it missed via this function. Members have NO direct
|
|
47908
|
+
-- grant on __lattice_changes (reading it raw would leak every change on the
|
|
47909
|
+
-- cloud), so this SECURITY DEFINER function is the only path and it returns ONLY
|
|
47910
|
+
-- the rows the CALLING role can see: keyed on session_user via lattice_row_visible
|
|
47911
|
+
-- (same gate as live fan-out, #4.3). Deletes are excluded \u2014 the ownership record
|
|
47912
|
+
-- is gone post-delete so visibility can't be verified, and replaying them would
|
|
47913
|
+
-- leak deleted-row pks (the client reconciles deletes on its reconnect refetch).
|
|
47914
|
+
-- Bounded (LIMIT clamped \u2264 1000) so a long gap can't stream the whole table (Rule:
|
|
47915
|
+
-- bounded reads on a hot path).
|
|
47916
|
+
CREATE OR REPLACE FUNCTION lattice_changes_since(p_seq bigint, p_limit int)
|
|
47917
|
+
RETURNS TABLE(seq bigint, table_name text, pk text, op text, owner_role text, created_at timestamptz)
|
|
47918
|
+
LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
47919
|
+
SELECT c."seq", c."table_name", c."pk", c."op", c."owner_role", c."created_at"
|
|
47920
|
+
FROM "__lattice_changes" c
|
|
47921
|
+
WHERE c."seq" > p_seq
|
|
47922
|
+
AND c."op" = 'upsert'
|
|
47923
|
+
AND lattice_row_visible(c."table_name", c."pk")
|
|
47924
|
+
ORDER BY c."seq" ASC
|
|
47925
|
+
LIMIT GREATEST(0, LEAST(COALESCE(p_limit, 500), 1000));
|
|
47926
|
+
$fn$;
|
|
47927
|
+
|
|
47928
|
+
-- #2.1 \u2014 per-row access summary for the connecting role. The GUI attaches this as
|
|
47929
|
+
-- each row's _access so the sharing affordance renders, but __lattice_owners is
|
|
47930
|
+
-- owner-only bookkeeping (members have no grant), so a member reading it directly
|
|
47931
|
+
-- got "permission denied". This SECURITY DEFINER function returns visibility +
|
|
47932
|
+
-- whether the CALLER owns the row, ONLY for the rows the caller can actually see
|
|
47933
|
+
-- (lattice_row_visible, keyed on session_user) \u2014 so a member learns nothing about
|
|
47934
|
+
-- rows hidden from it. Member-callable; the owner gets the same view of its rows.
|
|
47935
|
+
CREATE OR REPLACE FUNCTION lattice_rows_access(p_table text, p_pks text[])
|
|
47936
|
+
RETURNS TABLE(pk text, visibility text, owned boolean)
|
|
47937
|
+
LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
47938
|
+
SELECT o."pk", o."visibility", (o."owner_role" = session_user) AS owned
|
|
47939
|
+
FROM "__lattice_owners" o
|
|
47940
|
+
WHERE o."table_name" = p_table
|
|
47941
|
+
AND o."pk" = ANY(p_pks)
|
|
47942
|
+
AND lattice_row_visible(o."table_name", o."pk");
|
|
47943
|
+
$fn$;
|
|
47944
|
+
|
|
47945
|
+
-- #2.1 \u2014 grantees of a CALLER-OWNED custom-shared row (who you shared YOUR row
|
|
47946
|
+
-- with). Only the row owner sees this (the WHERE pins owner_role = session_user),
|
|
47947
|
+
-- so a member can't enumerate another owner's grants. __lattice_row_grants is
|
|
47948
|
+
-- member-ungranted, so this SECURITY DEFINER function is the member-safe path.
|
|
47949
|
+
CREATE OR REPLACE FUNCTION lattice_row_grantees(p_table text, p_pks text[])
|
|
47950
|
+
RETURNS TABLE(pk text, grantee_role text)
|
|
47951
|
+
LANGUAGE sql STABLE SECURITY DEFINER AS $fn$
|
|
47952
|
+
SELECT g."pk", g."grantee_role"
|
|
47953
|
+
FROM "__lattice_row_grants" g
|
|
47954
|
+
JOIN "__lattice_owners" o ON o."table_name" = g."table_name" AND o."pk" = g."pk"
|
|
47955
|
+
WHERE g."table_name" = p_table
|
|
47956
|
+
AND g."pk" = ANY(p_pks)
|
|
47957
|
+
AND o."owner_role" = session_user;
|
|
47958
|
+
$fn$;
|
|
47798
47959
|
`;
|
|
47799
47960
|
function tableRlsSql(table, pkCols) {
|
|
47800
47961
|
const q3 = `"${table.replace(/"/g, '""')}"`;
|
|
@@ -47862,28 +48023,14 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
|
|
|
47862
48023
|
async function installCloudRls(db) {
|
|
47863
48024
|
if (!isPg(db)) return;
|
|
47864
48025
|
const schema = await cloudSchema(db);
|
|
47865
|
-
const
|
|
47866
|
-
|
|
47867
|
-
// model (__lattice_cell_grants + lattice_cell_visible / lattice_grant_cell);
|
|
47868
|
-
// v6 added per-table policy (__lattice_table_policy: default_row_visibility +
|
|
47869
|
-
// never_share, enforced in the insert trigger + share/grant guards), the
|
|
47870
|
-
// canonical column-audience store (__lattice_column_policy), lattice_is_owner,
|
|
47871
|
-
// and the owner-only setters; v7 pins search_path on every SECURITY DEFINER
|
|
47872
|
-
// helper (closes the pg_temp-shadow RLS bypass) + revokes schema CREATE from
|
|
47873
|
-
// PUBLIC. The bootstrap is fully idempotent.
|
|
47874
|
-
version: "internal:cloud-rls:bootstrap:v7",
|
|
47875
|
-
sql: pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema)
|
|
47876
|
-
};
|
|
47877
|
-
await db.migrate([migration]);
|
|
48026
|
+
const sql = pinDefinerSearchPath(CLOUD_RLS_BOOTSTRAP_SQL, schema) + revokeSchemaCreateSql(schema);
|
|
48027
|
+
await runCloudBootstrapSql(db, sql);
|
|
47878
48028
|
}
|
|
47879
48029
|
async function enableChangelogRls(db) {
|
|
47880
48030
|
if (!isPg(db)) return;
|
|
47881
|
-
|
|
47882
|
-
|
|
47883
|
-
|
|
47884
|
-
// existing clouds.
|
|
47885
|
-
version: "internal:cloud-rls:changelog:v2",
|
|
47886
|
-
sql: `
|
|
48031
|
+
await runCloudBootstrapSql(
|
|
48032
|
+
db,
|
|
48033
|
+
`
|
|
47887
48034
|
ALTER TABLE "__lattice_changelog" ENABLE ROW LEVEL SECURITY;
|
|
47888
48035
|
ALTER TABLE "__lattice_changelog" FORCE ROW LEVEL SECURITY;
|
|
47889
48036
|
GRANT SELECT, INSERT ON "__lattice_changelog" TO ${MEMBER_GROUP};
|
|
@@ -47893,6 +48040,7 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
|
|
|
47893
48040
|
CASE
|
|
47894
48041
|
WHEN "change_kind" = 'derived' THEN
|
|
47895
48042
|
"source_ref" IS NOT NULL
|
|
48043
|
+
AND jsonb_array_length("source_ref"::jsonb) > 0
|
|
47896
48044
|
AND NOT EXISTS (
|
|
47897
48045
|
SELECT 1 FROM jsonb_array_elements_text("source_ref"::jsonb) AS src(sid)
|
|
47898
48046
|
WHERE NOT lattice_source_visible(src.sid)
|
|
@@ -47903,8 +48051,7 @@ CREATE POLICY "lattice_changelog_sel" ON "__lattice_changelog" FOR SELECT USING
|
|
|
47903
48051
|
DROP POLICY IF EXISTS "lattice_changelog_ins" ON "__lattice_changelog";
|
|
47904
48052
|
CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH CHECK (true);
|
|
47905
48053
|
`
|
|
47906
|
-
|
|
47907
|
-
await db.migrate([migration]);
|
|
48054
|
+
);
|
|
47908
48055
|
}
|
|
47909
48056
|
async function enableRlsForTable(db, table, pkCols) {
|
|
47910
48057
|
if (!isPg(db)) return;
|
|
@@ -47958,7 +48105,12 @@ async function provisionMemberRole(db, role, password) {
|
|
|
47958
48105
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
|
|
47959
48106
|
CREATE ROLE "${role}" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '${password}';
|
|
47960
48107
|
ELSE
|
|
47961
|
-
|
|
48108
|
+
-- Re-invite of an EXISTING role: set ONLY what changed (login + password).
|
|
48109
|
+
-- Restating NOSUPERUSER/superuser-class attrs trips Supabase supautils
|
|
48110
|
+
-- ("only superuser may alter the SUPERUSER attribute", 42501) since the
|
|
48111
|
+
-- owner 'postgres' isn't a true superuser. The role was already created
|
|
48112
|
+
-- NOSUPERUSER NOCREATEDB NOCREATEROLE, so there is nothing to restate.
|
|
48113
|
+
ALTER ROLE "${role}" WITH LOGIN PASSWORD '${password}';
|
|
47962
48114
|
END IF;
|
|
47963
48115
|
END $LATTICE$`
|
|
47964
48116
|
);
|
|
@@ -47997,9 +48149,28 @@ async function revokeCell(db, table, pk, column, grantee) {
|
|
|
47997
48149
|
async function revokeMemberRole(db, role) {
|
|
47998
48150
|
assertPg(db);
|
|
47999
48151
|
if (!ROLE_RE.test(role)) throw new Error(`lattice: invalid member role name "${role}"`);
|
|
48000
|
-
|
|
48152
|
+
const exists = await getAsyncOrSync(
|
|
48153
|
+
db.adapter,
|
|
48154
|
+
`SELECT 1 AS x FROM pg_roles WHERE rolname = ?`,
|
|
48155
|
+
[role]
|
|
48156
|
+
);
|
|
48157
|
+
if (!exists) return;
|
|
48158
|
+
for (const stmt of [`REASSIGN OWNED BY "${role}" TO CURRENT_USER`, `DROP OWNED BY "${role}"`]) {
|
|
48159
|
+
try {
|
|
48160
|
+
await runAsyncOrSync(db.adapter, stmt);
|
|
48161
|
+
} catch (e6) {
|
|
48162
|
+
if (!isInsufficientPrivilege(e6)) throw e6;
|
|
48163
|
+
console.warn(
|
|
48164
|
+
`[cloud] "${stmt.split(" ").slice(0, 2).join(" ")} \u2026" skipped (insufficient privilege; a scoped member owns no objects): ${e6.message}`
|
|
48165
|
+
);
|
|
48166
|
+
}
|
|
48167
|
+
}
|
|
48001
48168
|
await runAsyncOrSync(db.adapter, `DROP ROLE IF EXISTS "${role}"`);
|
|
48002
48169
|
}
|
|
48170
|
+
function isInsufficientPrivilege(e6) {
|
|
48171
|
+
const err = e6 ?? {};
|
|
48172
|
+
return err.code === "42501" || /permission denied/i.test(err.message ?? "");
|
|
48173
|
+
}
|
|
48003
48174
|
|
|
48004
48175
|
// src/cloud/discover.ts
|
|
48005
48176
|
async function discoverCloudTables(db) {
|
|
@@ -48243,6 +48414,7 @@ var FoldCache = class {
|
|
|
48243
48414
|
};
|
|
48244
48415
|
|
|
48245
48416
|
// src/cloud/settings.ts
|
|
48417
|
+
var import_node_crypto14 = require("crypto");
|
|
48246
48418
|
var CLOUD_SETTING_SYSTEM_PROMPT = "chat_system_prompt";
|
|
48247
48419
|
var CLOUD_SETTINGS_BOOTSTRAP_SQL = `
|
|
48248
48420
|
-- Owner-controlled, cloud-wide key/value settings. No grant to the member group,
|
|
@@ -48282,13 +48454,7 @@ END $fn$;
|
|
|
48282
48454
|
async function installCloudSettings(db) {
|
|
48283
48455
|
if (db.getDialect() !== "postgres") return;
|
|
48284
48456
|
const schema = await cloudSchema(db);
|
|
48285
|
-
|
|
48286
|
-
// v2 pins search_path on the two SECURITY DEFINER helpers (closes the
|
|
48287
|
-
// pg_temp-shadow class of bypass on the settings getter/setter).
|
|
48288
|
-
version: "internal:cloud-settings:v2",
|
|
48289
|
-
sql: pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema)
|
|
48290
|
-
};
|
|
48291
|
-
await db.migrate([migration]);
|
|
48457
|
+
await runCloudBootstrapSql(db, pinDefinerSearchPath(CLOUD_SETTINGS_BOOTSTRAP_SQL, schema));
|
|
48292
48458
|
}
|
|
48293
48459
|
async function getCloudSetting(db, key) {
|
|
48294
48460
|
if (db.getDialect() !== "postgres") return null;
|
|
@@ -48307,6 +48473,54 @@ async function setCloudSetting(db, key, value) {
|
|
|
48307
48473
|
}
|
|
48308
48474
|
|
|
48309
48475
|
// src/cloud/setup.ts
|
|
48476
|
+
var PRIVATE_ONLY_TABLES = [...NATIVE_INTERNAL_NAMES, "secrets"];
|
|
48477
|
+
async function reconcileCloudMemberAccess(db) {
|
|
48478
|
+
if (db.getDialect() !== "postgres") return;
|
|
48479
|
+
const registered = db.getRegisteredTableNames();
|
|
48480
|
+
for (const t8 of PRIVATE_ONLY_TABLES) {
|
|
48481
|
+
if (!registered.includes(t8)) continue;
|
|
48482
|
+
await runAsyncOrSync(
|
|
48483
|
+
db.adapter,
|
|
48484
|
+
`SELECT lattice_set_table_never_share('${t8.replace(/'/g, "''")}', true)`
|
|
48485
|
+
);
|
|
48486
|
+
}
|
|
48487
|
+
const rlsRows = await allAsyncOrSync(
|
|
48488
|
+
db.adapter,
|
|
48489
|
+
`SELECT c.relname AS name FROM pg_class c
|
|
48490
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
48491
|
+
WHERE n.nspname = current_schema() AND c.relkind = 'r' AND c.relrowsecurity`
|
|
48492
|
+
);
|
|
48493
|
+
const rlsOn = new Set(rlsRows.map((r6) => r6.name));
|
|
48494
|
+
for (const table of registered) {
|
|
48495
|
+
if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) continue;
|
|
48496
|
+
if (!rlsOn.has(table)) continue;
|
|
48497
|
+
if (db.getPrimaryKey(table).length === 0) continue;
|
|
48498
|
+
const q3 = `"${table.replace(/"/g, '""')}"`;
|
|
48499
|
+
const masked = tableNeedsAudienceView(db.getColumnAudience(table) ?? {});
|
|
48500
|
+
if (masked) {
|
|
48501
|
+
const v2 = `"${`${table}_v`.replace(/"/g, '""')}"`;
|
|
48502
|
+
await runAsyncOrSync(db.adapter, `GRANT SELECT ON ${v2} TO ${MEMBER_GROUP}`);
|
|
48503
|
+
await runAsyncOrSync(db.adapter, `GRANT INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`);
|
|
48504
|
+
} else {
|
|
48505
|
+
await runAsyncOrSync(
|
|
48506
|
+
db.adapter,
|
|
48507
|
+
`GRANT SELECT, INSERT, UPDATE, DELETE ON ${q3} TO ${MEMBER_GROUP}`
|
|
48508
|
+
);
|
|
48509
|
+
}
|
|
48510
|
+
}
|
|
48511
|
+
}
|
|
48512
|
+
async function secureNewCloudTable(db, table, pk) {
|
|
48513
|
+
if (db.getDialect() !== "postgres") return;
|
|
48514
|
+
if (table.startsWith("__lattice_") || table.startsWith("_lattice_")) return;
|
|
48515
|
+
if (pk.length === 0) return;
|
|
48516
|
+
await backfillOwnership(db, table, pk);
|
|
48517
|
+
await enableRlsForTable(db, table, pk);
|
|
48518
|
+
const cols = db.getRegisteredColumns(table);
|
|
48519
|
+
if (cols) {
|
|
48520
|
+
await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
|
|
48521
|
+
await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
48522
|
+
}
|
|
48523
|
+
}
|
|
48310
48524
|
async function secureCloud(db) {
|
|
48311
48525
|
if (db.getDialect() !== "postgres") return;
|
|
48312
48526
|
await installCloudRls(db);
|
|
@@ -48315,20 +48529,9 @@ async function secureCloud(db) {
|
|
|
48315
48529
|
await enableChangelogRls(db);
|
|
48316
48530
|
const registered = db.getRegisteredTableNames();
|
|
48317
48531
|
for (const table of registered) {
|
|
48318
|
-
|
|
48319
|
-
const pk = db.getPrimaryKey(table);
|
|
48320
|
-
if (pk.length === 0) continue;
|
|
48321
|
-
await backfillOwnership(db, table, pk);
|
|
48322
|
-
await enableRlsForTable(db, table, pk);
|
|
48323
|
-
const cols = db.getRegisteredColumns(table);
|
|
48324
|
-
if (cols) {
|
|
48325
|
-
await seedColumnPolicyFromYaml(db, table, db.getColumnAudience(table));
|
|
48326
|
-
await regenerateAudienceViewFromDb(db, table, Object.keys(cols), pk);
|
|
48327
|
-
}
|
|
48328
|
-
}
|
|
48329
|
-
if (registered.includes("secrets")) {
|
|
48330
|
-
await runAsyncOrSync(db.adapter, `SELECT lattice_set_table_never_share('secrets', true)`);
|
|
48532
|
+
await secureNewCloudTable(db, table, db.getPrimaryKey(table));
|
|
48331
48533
|
}
|
|
48534
|
+
await reconcileCloudMemberAccess(db);
|
|
48332
48535
|
await runAsyncOrSync(
|
|
48333
48536
|
db.adapter,
|
|
48334
48537
|
`DO $LATTICE$ BEGIN
|