okmodel 0.1.0 → 0.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 +237 -19
- package/dist/adapters/capabilities.d.ts +34 -0
- package/dist/adapters/pg/bunsql-extra.d.ts +17 -0
- package/dist/adapters/pg/bunsql.d.ts +60 -0
- package/dist/adapters/pg/call.d.ts +2 -12
- package/dist/adapters/pg/nodepostgres-extra.d.ts +18 -0
- package/dist/adapters/pg/nodepostgres.d.ts +57 -0
- package/dist/adapters/pg/postgres-extra.d.ts +50 -1
- package/dist/adapters/pg/postgresjs.d.ts +0 -4
- package/dist/adapters/pg/session.d.ts +94 -0
- package/dist/adapters/pg/watch.d.ts +18 -0
- package/dist/contracts/catalog/object.d.ts +1 -0
- package/dist/contracts/catalog/types.d.ts +15 -8
- package/dist/contracts/connection.d.ts +13 -0
- package/dist/contracts/driver.d.ts +18 -5
- package/dist/contracts/error.d.ts +17 -1
- package/dist/contracts/generator.d.ts +63 -0
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/internal.js +11 -9
- package/dist/contracts/location.d.ts +24 -0
- package/dist/contracts/rows.d.ts +15 -0
- package/dist/dialects/pg/archive-bind.d.ts +22 -0
- package/dist/dialects/pg/column.d.ts +126 -9
- package/dist/dialects/pg/compile.d.ts +1 -0
- package/dist/dialects/pg/custom.d.ts +6 -0
- package/dist/dialects/pg/filter-parse.d.ts +19 -0
- package/dist/dialects/pg/filters.d.ts +19 -0
- package/dist/dialects/pg/index.d.ts +11 -2
- package/dist/dialects/pg/index.js +159 -1063
- package/dist/dialects/pg/keys.d.ts +19 -3
- package/dist/dialects/pg/model.d.ts +223 -2
- package/dist/dialects/pg/operator-fit.d.ts +86 -0
- package/dist/dialects/pg/operators.d.ts +39 -1
- package/dist/dialects/pg/ops/arr-append.d.ts +9 -0
- package/dist/dialects/pg/ops/arr-ns.d.ts +7 -0
- package/dist/dialects/pg/ops/arr-remove.d.ts +9 -0
- package/dist/dialects/pg/ops/containedBy.d.ts +9 -0
- package/dist/dialects/pg/ops/contains.d.ts +8 -4
- package/dist/dialects/pg/ops/hasAnyKey.d.ts +8 -0
- package/dist/dialects/pg/ops/hasKey.d.ts +8 -0
- package/dist/dialects/pg/ops/json-ns.d.ts +6 -0
- package/dist/dialects/pg/ops/json-set.d.ts +12 -0
- package/dist/dialects/pg/ops/matches.d.ts +16 -0
- package/dist/dialects/pg/ops/overlaps.d.ts +9 -0
- package/dist/dialects/pg/ops/path.d.ts +13 -0
- package/dist/dialects/pg/preset.d.ts +76 -0
- package/dist/dialects/pg/relations.d.ts +59 -2
- package/dist/dialects/pg/schema.d.ts +109 -5
- package/dist/dialects/pg/table.d.ts +130 -11
- package/dist/dialects/pg/temporal.d.ts +9 -7
- package/dist/dialects/pg/tenancy.d.ts +94 -0
- package/dist/dialects/pg/through.d.ts +21 -0
- package/dist/dialects/pg/trait.d.ts +141 -0
- package/dist/dialects/pg/where.d.ts +37 -0
- package/dist/runtime/aggregate.d.ts +25 -0
- package/dist/runtime/archive-handle.d.ts +18 -0
- package/dist/runtime/archive-rules.d.ts +12 -0
- package/dist/runtime/archive.d.ts +73 -0
- package/dist/runtime/batch.d.ts +20 -0
- package/dist/runtime/client.d.ts +114 -5
- package/dist/runtime/conflict.d.ts +2 -1
- package/dist/runtime/exposure.d.ts +27 -0
- package/dist/runtime/hookm.d.ts +39 -0
- package/dist/runtime/ids/index.d.ts +11 -0
- package/dist/runtime/ids/index.js +193 -0
- package/dist/runtime/ids/okid-extended.d.ts +19 -0
- package/dist/runtime/ids/okid-generator.d.ts +21 -0
- package/dist/runtime/ids/okid-shared.d.ts +89 -0
- package/dist/runtime/ids/okid.d.ts +24 -0
- package/dist/runtime/ids/uuid.d.ts +21 -0
- package/dist/runtime/lock.d.ts +22 -0
- package/dist/runtime/operator-sql.d.ts +8 -0
- package/dist/runtime/page.d.ts +25 -0
- package/dist/runtime/pg/bun.d.ts +30 -0
- package/dist/runtime/pg/bun.js +52 -0
- package/dist/runtime/pg/pg.d.ts +29 -0
- package/dist/runtime/pg/pg.js +52 -0
- package/dist/runtime/pg/pglite.js +16 -17
- package/dist/runtime/pg/postgresjs.js +14 -24
- package/dist/runtime/plan.d.ts +146 -7
- package/dist/runtime/preset-stack.d.ts +17 -0
- package/dist/runtime/presets.d.ts +48 -0
- package/dist/runtime/read-stream.d.ts +26 -0
- package/dist/runtime/safety/index.d.ts +159 -0
- package/dist/runtime/safety/index.js +294 -0
- package/dist/runtime/safety-hook.d.ts +42 -0
- package/dist/runtime/server-check.d.ts +34 -0
- package/dist/runtime/tenancy/index.d.ts +37 -0
- package/dist/runtime/tenancy/index.js +365 -0
- package/dist/runtime/trait-read.d.ts +30 -0
- package/dist/runtime/traits/index.d.ts +128 -0
- package/dist/runtime/traits/index.js +509 -0
- package/dist/runtime/transaction.d.ts +19 -0
- package/dist/runtime/tx.d.ts +22 -7
- package/dist/runtime/types.d.ts +390 -25
- package/dist/runtime/validate/bind.d.ts +11 -0
- package/dist/runtime/validate/closed.d.ts +16 -0
- package/dist/runtime/validate/engine.d.ts +41 -0
- package/dist/runtime/validate/hook.d.ts +17 -0
- package/dist/runtime/validate/index.d.ts +21 -0
- package/dist/runtime/validate/index.js +175 -0
- package/dist/runtime/validate/load.d.ts +13 -0
- package/dist/runtime/validate/places.d.ts +41 -0
- package/dist/runtime/validate/rules.d.ts +108 -0
- package/dist/runtime/validate/surface.d.ts +102 -0
- package/dist/runtime/write.d.ts +30 -5
- package/dist/shared/{fgqgsb5z.js → 0qkxbm1f.js} +2 -2
- package/dist/shared/0v1g424p.js +26 -0
- package/dist/shared/10tjenmd.js +210 -0
- package/dist/shared/{tb4ze1tr.js → 1dp5knt7.js} +95 -10
- package/dist/shared/38crxbgn.js +229 -0
- package/dist/shared/3a5ne750.js +158 -0
- package/dist/shared/4b9da9d7.js +138 -0
- package/dist/shared/4e4c6vq2.js +62 -0
- package/dist/shared/{ckb1p0a4.js → 4jzzghr6.js} +416 -83
- package/dist/shared/{ycwz42az.js → 52szzakm.js} +6 -26
- package/dist/shared/{kynx7vbm.js → 5z4q2zda.js} +604 -21
- package/dist/shared/672t5vym.js +12 -0
- package/dist/shared/7m88x3e9.js +45 -0
- package/dist/shared/7mht49yd.js +1091 -0
- package/dist/shared/{mtkwv8wx.js → 7p2wbz7f.js} +1 -1
- package/dist/shared/83eaefwr.js +10 -0
- package/dist/shared/96ps4g1z.js +32 -0
- package/dist/shared/97g2jnrw.js +76 -0
- package/dist/shared/{xy79j5hq.js → 9k6vetvf.js} +25 -36
- package/dist/shared/{w8samvk1.js → a1gdp7j9.js} +1 -1
- package/dist/shared/a5sxz52y.js +13 -0
- package/dist/shared/{dynq16vb.js → b066g80h.js} +8 -6
- package/dist/shared/cstgkzdx.js +154 -0
- package/dist/shared/{ax16zpvm.js → d04j53v8.js} +6 -5
- package/dist/shared/{y9m15dfv.js → d2whjz4k.js} +15 -9
- package/dist/shared/dd4f8mnx.js +307 -0
- package/dist/shared/e6wg00wd.js +164 -0
- package/dist/shared/e7a268fy.js +195 -0
- package/dist/shared/{vkm3q4x1.js → eaxwrgby.js} +4 -3
- package/dist/shared/{7ck0dtry.js → ecdzej21.js} +160 -81
- package/dist/shared/{px90pf9p.js → edwqqryd.js} +21 -16
- package/dist/shared/{g2v6gnc8.js → eqmtn1bj.js} +57 -5
- package/dist/shared/g1b5zzj9.js +75 -0
- package/dist/shared/g9f0zqce.js +376 -0
- package/dist/shared/ge03bfca.js +64 -0
- package/dist/shared/{79ggp945.js → jns2wx4t.js} +55 -12
- package/dist/shared/k3ted12y.js +33 -0
- package/dist/shared/k7dwkhvq.js +31 -0
- package/dist/shared/{hd293j9g.js → kyxb62mc.js} +14 -2
- package/dist/shared/m9tv7xn7.js +404 -0
- package/dist/shared/mrq77sgw.js +55 -0
- package/dist/shared/mxmcdsh5.js +450 -0
- package/dist/shared/n442h54c.js +14 -0
- package/dist/shared/n47rd821.js +442 -0
- package/dist/shared/n4jqhpqz.js +27 -0
- package/dist/shared/{mkjmqjv4.js → nfzc5z0w.js} +2 -104
- package/dist/shared/nsva2a6b.js +42 -0
- package/dist/shared/ps43134h.js +69 -0
- package/dist/shared/{8b04d28c.js → py3nh7jy.js} +2 -2
- package/dist/shared/qa5dsc7z.js +86 -0
- package/dist/shared/{g5fhqmwe.js → qxa0yb9j.js} +6 -4
- package/dist/shared/r6nwkxnt.js +86 -0
- package/dist/shared/{4j72449d.js → rtnaqre0.js} +3 -3
- package/dist/shared/rwmrejjz.js +26 -0
- package/dist/shared/{zhc5vmgf.js → s6gqfn5j.js} +2 -2
- package/dist/shared/sacqha1y.js +70 -0
- package/dist/shared/{qk3hxfja.js → sfh4pk75.js} +5 -5
- package/dist/shared/{j5cn2963.js → t4tbsy9h.js} +13 -64
- package/dist/shared/{ngk8gj61.js → tbszx8mz.js} +15 -1
- package/dist/shared/v18jva5b.js +90 -0
- package/dist/shared/x9v1qgeb.js +201 -0
- package/dist/shared/xatfg0gn.js +193 -0
- package/dist/shared/xkz8g6by.js +225 -0
- package/dist/shared/{f5thc1s0.js → xrfnq5k8.js} +237 -418
- package/dist/shared/ytg3xf9p.js +149 -0
- package/dist/shared/yw5c610b.js +48 -0
- package/dist/shared/zd93g3rq.js +31 -0
- package/dist/shared/zt8y0wzb.js +132 -0
- package/dist/shared/{p0cbr3g2.js → zwmqxb0g.js} +1 -1
- package/dist/tooling/cli.js +14 -10
- package/dist/tooling/migrate/commands.d.ts +10 -0
- package/dist/tooling/migrate/engine.d.ts +36 -0
- package/package.json +56 -4
- package/dist/shared/gk7cs7sp.js +0 -566
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uuid2
|
|
3
|
+
} from "../../shared/ps43134h.js";
|
|
4
|
+
import {
|
|
5
|
+
ColumnBuilder2,
|
|
6
|
+
retarget
|
|
7
|
+
} from "../../shared/dd4f8mnx.js";
|
|
8
|
+
import {
|
|
9
|
+
definition,
|
|
10
|
+
unavailable
|
|
11
|
+
} from "../../shared/xatfg0gn.js";
|
|
12
|
+
import {
|
|
13
|
+
quote
|
|
14
|
+
} from "../../shared/xrfnq5k8.js";
|
|
15
|
+
import {
|
|
16
|
+
assertIdentifier2
|
|
17
|
+
} from "../../shared/nfzc5z0w.js";
|
|
18
|
+
import {
|
|
19
|
+
OkmError2
|
|
20
|
+
} from "../../shared/52szzakm.js";
|
|
21
|
+
|
|
22
|
+
// src/runtime/tenancy/index.ts
|
|
23
|
+
function global(reason) {
|
|
24
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
25
|
+
definition("global() needs a reason.");
|
|
26
|
+
}
|
|
27
|
+
return { kind: "global", reason: reason.trim() };
|
|
28
|
+
}
|
|
29
|
+
function columnTenancy(input) {
|
|
30
|
+
if (input === undefined || typeof input !== "object") {
|
|
31
|
+
definition("columnTenancy() needs { key, type }.");
|
|
32
|
+
}
|
|
33
|
+
if (input.type !== "uuid") {
|
|
34
|
+
definition(`columnTenancy() type ${String(input.type)} must be uuid.`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof input.key !== "string" || input.key.length === 0) {
|
|
37
|
+
definition("columnTenancy() key must be a field name.");
|
|
38
|
+
}
|
|
39
|
+
assertIdentifier2(input.key, "tenancy key");
|
|
40
|
+
const key = input.key;
|
|
41
|
+
const column = uuid2().guarded();
|
|
42
|
+
const encode = column.state.encode;
|
|
43
|
+
const tenants = new Set;
|
|
44
|
+
const globals = new Map;
|
|
45
|
+
const exemptions = new Map;
|
|
46
|
+
const api = {
|
|
47
|
+
key,
|
|
48
|
+
type: "uuid",
|
|
49
|
+
strategy: "column",
|
|
50
|
+
rewrite(tables) {
|
|
51
|
+
tenants.clear();
|
|
52
|
+
globals.clear();
|
|
53
|
+
exemptions.clear();
|
|
54
|
+
return rewriteTables(key, column, tables, tenants, globals, exemptions);
|
|
55
|
+
},
|
|
56
|
+
column: () => column,
|
|
57
|
+
predicate(spec) {
|
|
58
|
+
return writePredicate(key, tenants, spec);
|
|
59
|
+
},
|
|
60
|
+
guard(table, field, kind) {
|
|
61
|
+
if (!tenants.has(table) || field !== key)
|
|
62
|
+
return;
|
|
63
|
+
refuseKey(kind, table, field);
|
|
64
|
+
},
|
|
65
|
+
stamp(table, rows, scope) {
|
|
66
|
+
if (!tenants.has(table))
|
|
67
|
+
return;
|
|
68
|
+
stampRows(key, table, rows, scope);
|
|
69
|
+
},
|
|
70
|
+
client(spec) {
|
|
71
|
+
const names = spec.scoped ? spec.names : spec.names.filter((name) => !tenants.has(name));
|
|
72
|
+
if (spec.scoped)
|
|
73
|
+
return { names };
|
|
74
|
+
return { names, ...bindClient(key, encode, spec.open) };
|
|
75
|
+
},
|
|
76
|
+
rules(table, source, scope) {
|
|
77
|
+
return tenancyRules(table, source, scope, tenants, globals, exemptions);
|
|
78
|
+
},
|
|
79
|
+
hook(target, ctx) {
|
|
80
|
+
if (ctx.tables === undefined || ctx.names === undefined || ctx.open === undefined || ctx.scoped === undefined) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const view = api.client({ names: ctx.names, scoped: ctx.scoped, open: ctx.open });
|
|
84
|
+
if (!ctx.scoped) {
|
|
85
|
+
for (const name of ctx.names) {
|
|
86
|
+
if (view.names.includes(name))
|
|
87
|
+
continue;
|
|
88
|
+
delete ctx.tables[name];
|
|
89
|
+
delete target[name];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (view.for !== undefined)
|
|
93
|
+
target.for = view.for;
|
|
94
|
+
if (view.unscoped !== undefined)
|
|
95
|
+
target.unscoped = view.unscoped;
|
|
96
|
+
},
|
|
97
|
+
missing(table) {
|
|
98
|
+
throw new OkmError2("OKM1701", `Table ${table} is tenant-scoped. Call for({ ${key} }) or unscoped("reason").`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return api;
|
|
102
|
+
}
|
|
103
|
+
function rewriteTables(key, column, tables, tenants, globals, exemptions) {
|
|
104
|
+
const kind = new Map;
|
|
105
|
+
for (const item of tables) {
|
|
106
|
+
const options = item.options;
|
|
107
|
+
const mark = options?.tenancy;
|
|
108
|
+
if (mark === undefined) {
|
|
109
|
+
kind.set(item.name, "tenant");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (isGlobal(mark)) {
|
|
113
|
+
kind.set(item.name, "global");
|
|
114
|
+
globals.set(item.name, mark.reason);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (isRecord(mark) && "via" in mark) {
|
|
118
|
+
unavailable(`Table ${item.name} tenancy via is not available yet. It arrives later.`);
|
|
119
|
+
}
|
|
120
|
+
definition(`Table ${item.name} tenancy must be global("reason").`);
|
|
121
|
+
}
|
|
122
|
+
return tables.map((item) => {
|
|
123
|
+
if (kind.get(item.name) !== "tenant") {
|
|
124
|
+
if (kind.get(item.name) === "global")
|
|
125
|
+
refuseGlobalReference(item, kind);
|
|
126
|
+
return item;
|
|
127
|
+
}
|
|
128
|
+
return rewriteTenant(key, column, item, tables, kind, tenants, exemptions);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function refuseGlobalReference(item, kind) {
|
|
132
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
133
|
+
if (!(builder instanceof ColumnBuilder2))
|
|
134
|
+
continue;
|
|
135
|
+
const reference = builder.state.references;
|
|
136
|
+
if (reference !== undefined && kind.get(reference.table) === "tenant") {
|
|
137
|
+
throw new OkmError2("OKM1705", `Table ${item.name}.${field} is global and references tenant table ${reference.table}. Drop the reference, or make ${item.name} a tenant table.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function rewriteTenant(key, column, item, tables, kind, tenants, exemptions) {
|
|
142
|
+
if (Object.hasOwn(item.columns, key)) {
|
|
143
|
+
throw new OkmError2("OKM1012", `Table ${item.name} already declares ${key}, which tenancy adds. Rename the field or drop it from the table.`);
|
|
144
|
+
}
|
|
145
|
+
const options = item.options;
|
|
146
|
+
const columns = {};
|
|
147
|
+
const unique = {};
|
|
148
|
+
const named = options?.unique;
|
|
149
|
+
if (named !== undefined) {
|
|
150
|
+
for (const [name, fields] of Object.entries(named)) {
|
|
151
|
+
unique[name] = fields.includes(key) ? fields : [key, ...fields];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const notes = [];
|
|
155
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
156
|
+
if (!(builder instanceof ColumnBuilder2)) {
|
|
157
|
+
columns[field] = builder;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const patch = {};
|
|
161
|
+
if (builder.state.primaryKey === true)
|
|
162
|
+
patch.dropPrimary = true;
|
|
163
|
+
const flag = builder.state.unique;
|
|
164
|
+
if (flag !== undefined) {
|
|
165
|
+
if (flag.global === true) {
|
|
166
|
+
const reason = flag.reason?.trim() ?? "";
|
|
167
|
+
if (reason.length === 0) {
|
|
168
|
+
definition(`Unique ${item.name}.${field} sets global and needs a reason.`);
|
|
169
|
+
}
|
|
170
|
+
notes.push({ name: field, reason });
|
|
171
|
+
} else {
|
|
172
|
+
patch.dropUnique = true;
|
|
173
|
+
unique[field] = [key, field];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const reference = builder.state.references;
|
|
177
|
+
if (reference !== undefined) {
|
|
178
|
+
const widened = widenReference(key, item.name, field, reference, tables, kind);
|
|
179
|
+
if (widened !== undefined)
|
|
180
|
+
patch.references = widened;
|
|
181
|
+
}
|
|
182
|
+
const changed = patch.dropUnique === true || patch.dropPrimary === true || patch.references !== undefined;
|
|
183
|
+
columns[field] = changed ? retarget(builder, patch) : builder;
|
|
184
|
+
}
|
|
185
|
+
const primary = primaryFields(item);
|
|
186
|
+
const primaryKey = primary.length > 0 && !primary.includes(key) ? [...primary, key] : undefined;
|
|
187
|
+
if (primaryKey !== undefined)
|
|
188
|
+
unique[`${primary.join("_")}_${key}`] = primaryKey;
|
|
189
|
+
columns[key] = column;
|
|
190
|
+
tenants.add(item.name);
|
|
191
|
+
if (notes.length > 0)
|
|
192
|
+
exemptions.set(item.name, notes);
|
|
193
|
+
const indexes = wrapIndexes(key, item.name, options?.indexes);
|
|
194
|
+
return {
|
|
195
|
+
...item,
|
|
196
|
+
columns,
|
|
197
|
+
options: {
|
|
198
|
+
...options,
|
|
199
|
+
...primaryKey !== undefined ? { primaryKey } : {},
|
|
200
|
+
...Object.keys(unique).length > 0 ? { unique } : {},
|
|
201
|
+
...indexes !== undefined ? { indexes } : {}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function widenReference(key, table, field, reference, tables, kind) {
|
|
206
|
+
const targetKind = kind.get(reference.table);
|
|
207
|
+
if (targetKind !== "tenant")
|
|
208
|
+
return;
|
|
209
|
+
const action = [reference.onDelete, reference.onUpdate].find((item) => item === "set null" || item === "set default");
|
|
210
|
+
if (action !== undefined) {
|
|
211
|
+
definition(`Foreign key ${table}.${field} includes ${key}, so ${action} cannot clear it. Accepted actions: cascade, restrict, no action.`);
|
|
212
|
+
}
|
|
213
|
+
const target = tables.find((item) => item.name === reference.table);
|
|
214
|
+
const remote = reference.columns ?? (target === undefined ? undefined : primaryFields(target));
|
|
215
|
+
if (remote === undefined || remote.length === 0)
|
|
216
|
+
return;
|
|
217
|
+
if (remote.includes(key)) {
|
|
218
|
+
if (reference.along?.includes(key))
|
|
219
|
+
return;
|
|
220
|
+
return { ...reference, along: [...reference.along ?? [], key] };
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
...reference,
|
|
224
|
+
columns: [...remote, key],
|
|
225
|
+
along: [...reference.along ?? [], key]
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function primaryFields(item) {
|
|
229
|
+
const listed = item.options?.primaryKey;
|
|
230
|
+
if (listed !== undefined && listed.length > 0)
|
|
231
|
+
return [...listed];
|
|
232
|
+
const fields = [];
|
|
233
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
234
|
+
if (builder instanceof ColumnBuilder2 && builder.state.primaryKey === true)
|
|
235
|
+
fields.push(field);
|
|
236
|
+
}
|
|
237
|
+
return fields;
|
|
238
|
+
}
|
|
239
|
+
function wrapIndexes(key, table, build) {
|
|
240
|
+
if (build === undefined)
|
|
241
|
+
return;
|
|
242
|
+
return (columns) => {
|
|
243
|
+
const calls = build(columns);
|
|
244
|
+
const tenantSql = columns[key]?.name;
|
|
245
|
+
if (!Array.isArray(calls) || tenantSql === undefined)
|
|
246
|
+
return calls;
|
|
247
|
+
for (const call of calls) {
|
|
248
|
+
if (call.columns[0] === tenantSql)
|
|
249
|
+
continue;
|
|
250
|
+
throw new OkmError2("OKM1706", `Index on ${table} starts with ${call.columns[0] ?? "no column"}. Put ${tenantSql} first.`);
|
|
251
|
+
}
|
|
252
|
+
return calls;
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function writePredicate(key, tenants, input) {
|
|
256
|
+
if (!tenants.has(input.table))
|
|
257
|
+
return false;
|
|
258
|
+
const scope = input.scope;
|
|
259
|
+
if (scope === undefined) {
|
|
260
|
+
throw new OkmError2("OKM1701", `Table ${input.table} is tenant-scoped. Call for({ ${key} }) or unscoped("reason").`);
|
|
261
|
+
}
|
|
262
|
+
if ("unscoped" in scope) {
|
|
263
|
+
input.sink.mark("unscoped|");
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
const sql = input.fieldSql(key);
|
|
267
|
+
if (sql === undefined) {
|
|
268
|
+
throw new OkmError2("OKM1701", `Table ${input.table} is missing its tenant key ${key}.`);
|
|
269
|
+
}
|
|
270
|
+
input.sink.text(input.appended ? " and " : " where ");
|
|
271
|
+
input.sink.text(input.alias);
|
|
272
|
+
input.sink.text(".");
|
|
273
|
+
input.sink.text(quote(sql));
|
|
274
|
+
input.sink.text(" = ");
|
|
275
|
+
input.sink.param(input.encode === undefined ? scope.value : input.encode(scope.value));
|
|
276
|
+
input.sink.mark("tenant|");
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
function refuseKey(kind, table, field) {
|
|
280
|
+
if (kind === "insert") {
|
|
281
|
+
throw new OkmError2("OKM1190", `Field ${table}.${field} is the tenant key. The scope sets it. Input cannot.`);
|
|
282
|
+
}
|
|
283
|
+
if (kind === "update") {
|
|
284
|
+
throw new OkmError2("OKM1704", `Field ${table}.${field} is the tenant key. An update cannot change it.`);
|
|
285
|
+
}
|
|
286
|
+
throw new OkmError2("OKM1704", `Field ${table}.${field} is the tenant key. A filter cannot set it.`);
|
|
287
|
+
}
|
|
288
|
+
function stampRows(key, table, rows, scope) {
|
|
289
|
+
if (scope === undefined || "unscoped" in scope) {
|
|
290
|
+
throw new OkmError2("OKM1701", `insert on ${table} has no tenant. Call for({ ${key} }) so the scope can set it.`);
|
|
291
|
+
}
|
|
292
|
+
for (const row of rows)
|
|
293
|
+
row[key] = scope.value;
|
|
294
|
+
}
|
|
295
|
+
function bindClient(key, encode, openScope) {
|
|
296
|
+
return {
|
|
297
|
+
for: (input) => {
|
|
298
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
299
|
+
throw new OkmError2("OKM1701", `for() needs { ${key} }.`);
|
|
300
|
+
}
|
|
301
|
+
const record = input;
|
|
302
|
+
const value = record[key];
|
|
303
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
304
|
+
throw new OkmError2("OKM1701", `for() needs { ${key} } as a uuid.`);
|
|
305
|
+
}
|
|
306
|
+
for (const name of Object.keys(record)) {
|
|
307
|
+
if (name !== key) {
|
|
308
|
+
throw new OkmError2("OKM1701", `for() accepts ${key}. ${name} is not the tenant key.`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
encode(value);
|
|
312
|
+
return openScope({ value });
|
|
313
|
+
},
|
|
314
|
+
unscoped: (reason) => {
|
|
315
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
316
|
+
throw new OkmError2("OKM1701", "unscoped() needs a reason.");
|
|
317
|
+
}
|
|
318
|
+
return openScope({ unscoped: reason.trim() });
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function tenancyRules(table, source, scope, tenants, globals, exemptions) {
|
|
323
|
+
const rules = [];
|
|
324
|
+
const reason = globals.get(table);
|
|
325
|
+
if (reason !== undefined) {
|
|
326
|
+
rules.push({
|
|
327
|
+
rule: "tenancy",
|
|
328
|
+
contribution: `global ${reason}`,
|
|
329
|
+
provenance: "catalog",
|
|
330
|
+
...source !== undefined ? { source } : {}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const notes = exemptions.get(table);
|
|
334
|
+
if (notes !== undefined) {
|
|
335
|
+
for (const item of notes) {
|
|
336
|
+
rules.push({
|
|
337
|
+
rule: "tenancy",
|
|
338
|
+
contribution: `unique ${item.name} global ${item.reason}`,
|
|
339
|
+
provenance: "catalog",
|
|
340
|
+
...source !== undefined ? { source } : {}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (tenants.has(table) && scope !== undefined && "value" in scope) {
|
|
345
|
+
rules.push({ rule: "tenancy", contribution: "scoped", provenance: "planner" });
|
|
346
|
+
}
|
|
347
|
+
if (scope !== undefined && "unscoped" in scope) {
|
|
348
|
+
rules.push({
|
|
349
|
+
rule: "tenancy",
|
|
350
|
+
contribution: `unscoped ${scope.unscoped}`,
|
|
351
|
+
provenance: "caller"
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return rules;
|
|
355
|
+
}
|
|
356
|
+
function isGlobal(value) {
|
|
357
|
+
return isRecord(value) && value.kind === "global" && typeof value.reason === "string";
|
|
358
|
+
}
|
|
359
|
+
function isRecord(value) {
|
|
360
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
361
|
+
}
|
|
362
|
+
export {
|
|
363
|
+
columnTenancy,
|
|
364
|
+
global
|
|
365
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads sealed and touch columns off the traits stored on a table.
|
|
3
|
+
*
|
|
4
|
+
* `schema()` does not copy those lists onto the model. This module is
|
|
5
|
+
* imported by the write path, which loads on the first write.
|
|
6
|
+
*/
|
|
7
|
+
import type { TableModel } from "../dialects/pg/model.js";
|
|
8
|
+
/**
|
|
9
|
+
* Fields a trait sets to `now()` on update.
|
|
10
|
+
*
|
|
11
|
+
* @param model - Table model
|
|
12
|
+
* @returns Field names, or `undefined` when no trait touches a column
|
|
13
|
+
*/
|
|
14
|
+
export declare function touchFields(model: TableModel): readonly string[] | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a trait owns this field, so input cannot set it.
|
|
17
|
+
*
|
|
18
|
+
* @param model - Table model
|
|
19
|
+
* @param field - Field name
|
|
20
|
+
* @returns `true` when a trait seals it
|
|
21
|
+
*/
|
|
22
|
+
export declare function fieldSealed(model: TableModel, field: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The trait that seals a field, when one does.
|
|
25
|
+
*
|
|
26
|
+
* @param model - Table model
|
|
27
|
+
* @param field - Field name
|
|
28
|
+
* @returns The trait name, or `undefined`
|
|
29
|
+
*/
|
|
30
|
+
export declare function sealingTrait(model: TableModel, field: string): string | undefined;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traits (`okmodel/traits`).
|
|
3
|
+
*
|
|
4
|
+
* Importing this module is what puts a trait on a schema. An application that
|
|
5
|
+
* does not import it does not load these columns.
|
|
6
|
+
*/
|
|
7
|
+
import { type PresetMap } from "../../dialects/pg/preset.js";
|
|
8
|
+
import type { AnyTable, RowFrom } from "../../dialects/pg/table.js";
|
|
9
|
+
import { attachArchive } from "../archive-handle.js";
|
|
10
|
+
import { type Trait, type TraitContext, type TraitModel } from "../../dialects/pg/trait.js";
|
|
11
|
+
export type { Trait };
|
|
12
|
+
/**
|
|
13
|
+
* Options `trait()` accepts.
|
|
14
|
+
*
|
|
15
|
+
* `methods` and `requires` are named here and rejected until the prompt that
|
|
16
|
+
* implements them.
|
|
17
|
+
*
|
|
18
|
+
* @typeParam TFields - Columns the trait adds
|
|
19
|
+
* @typeParam TPresets - Presets the trait adds to each table it applies to
|
|
20
|
+
*/
|
|
21
|
+
export type TraitDefinition<TFields extends Readonly<Record<string, object>>, TPresets extends PresetMap<RowFrom<TFields>> = PresetMap<RowFrom<TFields>>> = {
|
|
22
|
+
readonly fields: TFields;
|
|
23
|
+
/** Columns set to `now()` on update. Each one is sealed. */
|
|
24
|
+
readonly touch?: readonly (keyof TFields & string)[];
|
|
25
|
+
/** Columns input cannot set, including `{ allow }`. */
|
|
26
|
+
readonly sealed?: readonly (keyof TFields & string)[];
|
|
27
|
+
/**
|
|
28
|
+
* Named query refinements added to every table the trait applies to.
|
|
29
|
+
*
|
|
30
|
+
* They see the trait's columns, not the table's. A name a table or another
|
|
31
|
+
* trait also defines is OKM1040, and so is a reserved name.
|
|
32
|
+
*/
|
|
33
|
+
readonly presets?: TPresets & PresetMap<RowFrom<TFields>>;
|
|
34
|
+
readonly methods?: unknown;
|
|
35
|
+
readonly requires?: unknown;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Builds a trait.
|
|
39
|
+
*
|
|
40
|
+
* The same function the built-in traits use. `schema()` copies `fields` onto
|
|
41
|
+
* each table and rejects a field the table already has (OKM1012).
|
|
42
|
+
*
|
|
43
|
+
* @typeParam TFields - Columns the trait adds
|
|
44
|
+
* @param name - Provenance name stored on those columns
|
|
45
|
+
* @param definition - Fields and, when needed, which ones the write path seals
|
|
46
|
+
* @returns The trait
|
|
47
|
+
*/
|
|
48
|
+
export declare function trait<const TFields extends Readonly<Record<string, object>>, const TPresets extends PresetMap<RowFrom<TFields>> = Record<never, never>>(name: string, input: TraitDefinition<TFields, TPresets>): {
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly fields: TFields;
|
|
51
|
+
readonly touch?: readonly (keyof TFields & string)[];
|
|
52
|
+
readonly sealed?: readonly (keyof TFields & string)[];
|
|
53
|
+
apply(model: TraitModel, ctx: TraitContext): void;
|
|
54
|
+
} & ([keyof TPresets] extends [never] ? unknown : {
|
|
55
|
+
readonly presets: TPresets;
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Options for {@link timestamps}.
|
|
59
|
+
*
|
|
60
|
+
* `enforce: "trigger"` is the database trigger. It arrives in 0.3.
|
|
61
|
+
*/
|
|
62
|
+
export type TimestampsOptions = {
|
|
63
|
+
readonly enforce?: "trigger";
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Adds `createdAt` and `updatedAt`.
|
|
67
|
+
*
|
|
68
|
+
* Both are `timestamptz not null default now()`, and input cannot set either
|
|
69
|
+
* of them. Insert leaves them to the default, so one statement stamps both
|
|
70
|
+
* with the same time. Update sets `updatedAt` to `now()` and leaves `createdAt`.
|
|
71
|
+
*
|
|
72
|
+
* @param options - Pass `{ enforce: "trigger" }` only when the trigger ships
|
|
73
|
+
* @returns The timestamps trait
|
|
74
|
+
*/
|
|
75
|
+
export declare function timestamps(options?: TimestampsOptions): {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly fields: {
|
|
78
|
+
readonly createdAt: import("../../dialects/pg/column.js").ColumnBuilder<Temporal.Instant, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "hasDefault">, "guarded">>;
|
|
79
|
+
readonly updatedAt: import("../../dialects/pg/column.js").ColumnBuilder<Temporal.Instant, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "hasDefault">, "guarded">>;
|
|
80
|
+
};
|
|
81
|
+
readonly touch?: readonly ("createdAt" | "updatedAt")[];
|
|
82
|
+
readonly sealed?: readonly ("createdAt" | "updatedAt")[];
|
|
83
|
+
apply(model: TraitModel, ctx: TraitContext): void;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Options for {@link archivable}.
|
|
87
|
+
*
|
|
88
|
+
* `strategy: "column"` keeps archived rows in the same table. `strategy: "table"`
|
|
89
|
+
* is deferred. `cascade` names the child tables archived and restored with the row.
|
|
90
|
+
*/
|
|
91
|
+
export type ArchivableOptions = {
|
|
92
|
+
/** `"table"` is reserved and throws OKM1061. */
|
|
93
|
+
readonly strategy?: "column" | "table";
|
|
94
|
+
readonly cascade?: readonly string[];
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Adds `archivedAt` and `archiveId`, and enables `archive()` and `restore()`.
|
|
98
|
+
*
|
|
99
|
+
* Reads, updates, and deletes target the active set. `withArchived()` and
|
|
100
|
+
* `onlyArchived()` widen or switch it. Uniques on the table become partial.
|
|
101
|
+
* `cascade` names children that share the call's `archiveId`.
|
|
102
|
+
*
|
|
103
|
+
* @param options - Column strategy and the child tables to cascade
|
|
104
|
+
* @returns The archivable trait
|
|
105
|
+
*/
|
|
106
|
+
export declare function archivable(options?: ArchivableOptions): {
|
|
107
|
+
name: string;
|
|
108
|
+
fields: {
|
|
109
|
+
readonly archivedAt: import("../../dialects/pg/column.js").ColumnBuilder<Temporal.Instant, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "nullable">, "guarded">>;
|
|
110
|
+
readonly archiveId: import("../../dialects/pg/column.js").ColumnBuilder<string, import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").FlagTrue<import("../../dialects/pg/column.js").PlainFlags, "nullable">, "guarded">>;
|
|
111
|
+
};
|
|
112
|
+
touch?: readonly ("archiveId" | "archivedAt")[];
|
|
113
|
+
sealed?: readonly ("archiveId" | "archivedAt")[];
|
|
114
|
+
"~archive": true;
|
|
115
|
+
apply(model: TraitModel, ctx: TraitContext): void;
|
|
116
|
+
/**
|
|
117
|
+
* Turns uniques into partial indexes and checks cascade.
|
|
118
|
+
*
|
|
119
|
+
* Runs in the schema rewrite, after tenancy widens uniques.
|
|
120
|
+
*
|
|
121
|
+
* @param tables - Tables after the tenancy rewrite
|
|
122
|
+
* @param casing - Schema casing
|
|
123
|
+
* @param schemaTraits - Traits from `schema()`, when any were passed
|
|
124
|
+
* @returns The rewritten tables
|
|
125
|
+
*/
|
|
126
|
+
rewrite(tables: readonly AnyTable[], casing: "snake" | undefined, schemaTraits: readonly object[] | undefined): readonly AnyTable[];
|
|
127
|
+
hook: typeof attachArchive;
|
|
128
|
+
};
|