@drhomet/mithaq 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/README.md +161 -0
- package/dist/asas/build.d.ts +10 -2
- package/dist/asas/build.js +69 -22
- package/dist/asas/cli/mithaq.js +35 -3
- package/dist/asas/cli/upgrade.d.ts +22 -9
- package/dist/asas/cli/upgrade.js +187 -78
- package/dist/asas/http/fetch.d.ts +21 -3
- package/dist/asas/http/fetch.js +123 -60
- package/dist/asas/http/orpc.d.ts +5 -3
- package/dist/asas/http/orpc.js +126 -108
- package/dist/asas/runner/bulk.d.ts +1 -1
- package/dist/asas/runner/bulk.js +16 -11
- package/dist/asas/runner/errors.d.ts +5 -2
- package/dist/asas/runner/errors.js +18 -2
- package/dist/asas/runner/read.d.ts +6 -2
- package/dist/asas/runner/read.js +28 -6
- package/dist/asas/runner/steps.d.ts +22 -4
- package/dist/asas/runner/steps.js +15 -9
- package/dist/asas/runner/store.d.ts +4 -0
- package/dist/asas/runner/store.js +21 -0
- package/dist/asas/runner/tenant.d.ts +2 -2
- package/dist/asas/runner/tenant.js +78 -16
- package/dist/asas/runner/write.d.ts +173 -11
- package/dist/asas/runner/write.js +274 -46
- package/dist/auth/better-auth/access.d.ts +14 -0
- package/dist/auth/better-auth/access.js +32 -0
- package/dist/auth/better-auth/generate.d.ts +3 -1
- package/dist/auth/better-auth/generate.js +26 -8
- package/dist/auth/better-auth/index.d.ts +2 -0
- package/dist/auth/better-auth/tenant.d.ts +11 -3
- package/dist/auth/better-auth/tenant.js +10 -2
- package/dist/auth/local/index.js +5 -3
- package/dist/base/fields.d.ts +3 -2
- package/dist/base/fields.js +3 -2
- package/dist/base/index.d.ts +2 -1
- package/dist/base/json-schema.d.ts +7 -0
- package/dist/base/json-schema.js +35 -0
- package/dist/base/kinds.d.ts +21 -0
- package/dist/base/kinds.js +46 -1
- package/dist/base/rules.d.ts +4 -4
- package/dist/base/rules.js +4 -3
- package/dist/drizzle/index.d.ts +9 -139
- package/dist/drizzle/index.js +143 -24
- package/dist/drizzle/kinds.d.ts +1 -0
- package/dist/drizzle/kinds.js +3 -1
- package/dist/drizzle/migrate.d.ts +26 -0
- package/dist/drizzle/migrate.js +69 -26
- package/dist/drizzle/rules.d.ts +1 -1
- package/dist/index.d.ts +11 -21
- package/dist/index.js +8 -18
- package/dist/mithaq/define.d.ts +62 -15
- package/dist/mithaq/define.js +23 -9
- package/dist/mithaq/generate-messages.js +5 -4
- package/dist/mithaq/language.d.ts +43 -4
- package/dist/mithaq/message-file.d.ts +13 -0
- package/dist/mithaq/message-file.js +29 -0
- package/dist/mithaq/messages.js +5 -0
- package/dist/mithaq/resolve.js +70 -10
- package/dist/mithaq/types.d.ts +29 -1
- package/dist/rbac/index.d.ts +2 -2
- package/dist/rbac/index.js +1 -1
- package/dist/rich-text/index.d.ts +1 -0
- package/dist/rich-text/index.js +1 -0
- package/dist/rich-text/rich-text.d.ts +2 -55
- package/dist/rich-text/rich-text.js +2 -125
- package/dist/rich-text/value.d.ts +55 -0
- package/dist/rich-text/value.js +126 -0
- package/dist/screen/client.d.ts +5 -2
- package/dist/screen/client.js +5 -2
- package/dist/screen/generate.js +48 -6
- package/dist/screen/index.d.ts +18 -1
- package/dist/screen/index.js +24 -6
- package/dist/screen/react.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -3
- package/dist/storage/index.js +1 -2
- package/dist/storage/storage.d.ts +2 -0
- package/dist/storage/storage.js +9 -2
- package/dist/targets.d.ts +14 -0
- package/dist/targets.js +16 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +81 -0
- package/dist/zod/index.d.ts +12 -3
- package/dist/zod/index.js +6 -5
- package/dist/zod/kinds.d.ts +3 -1
- package/dist/zod/kinds.js +11 -2
- package/dist/zod/rules.d.ts +1 -1
- package/docs/packages/mithaq/README.md +962 -0
- package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
- package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
- package/docs/packages/mithaq/src/drizzle/README.md +39 -0
- package/docs/packages/mithaq-admin/README.md +312 -0
- package/docs/templates/asas/README.md +1056 -0
- package/package.json +37 -8
- package/skills/add-field/SKILL.md +27 -0
- package/skills/add-kayan/SKILL.md +26 -0
- package/skills/eject-admin-piece/SKILL.md +28 -0
- package/skills/upgrade-mithaq/SKILL.md +35 -0
- package/r/mithaq-cells.json +0 -20
- package/r/mithaq-controls.json +0 -37
- package/r/mithaq-directional-icon.json +0 -15
- package/r/mithaq-expected-updated-at.json +0 -15
- package/r/mithaq-field-shell.json +0 -18
- package/r/mithaq-filter-operators.json +0 -15
- package/r/mithaq-form.json +0 -33
- package/r/mithaq-list-columns.json +0 -23
- package/r/mithaq-list-filters.json +0 -24
- package/r/mithaq-list-pagination.json +0 -24
- package/r/mithaq-list-query.json +0 -18
- package/r/mithaq-list-selection.json +0 -19
- package/r/mithaq-list.json +0 -33
- package/r/mithaq-nav.json +0 -24
- package/r/mithaq-nodes.json +0 -34
- package/r/mithaq-relationship-cell.json +0 -20
- package/r/mithaq-relationship-control.json +0 -20
- package/r/mithaq-rich-text.json +0 -40
- package/r/mithaq-screens-host.json +0 -96
- package/r/mithaq-storage.json +0 -44
- package/r/mithaq-upload.json +0 -29
- package/r/registry.json +0 -550
|
@@ -3,6 +3,7 @@ import { declaredFieldAccess } from '../../mithaq/rows.js';
|
|
|
3
3
|
import { providerOf } from '../../base/rules.js';
|
|
4
4
|
import { invalid } from './errors.js';
|
|
5
5
|
import { snapshot } from './runtime.js';
|
|
6
|
+
import { rawStepStore } from './store.js';
|
|
6
7
|
// The assertion checks the list stays inside `OperationStep<never>`, not the direction it names.
|
|
7
8
|
export const atEffect = (list) => list;
|
|
8
9
|
// A map only this module writes: a tag on the function would let any function buy
|
|
@@ -17,6 +18,12 @@ const seal = (name, step) => {
|
|
|
17
18
|
};
|
|
18
19
|
/** What position this step holds, or nothing at all for a step of the app's own. */
|
|
19
20
|
export const sealOf = (step) => seals.get(step);
|
|
21
|
+
// Each copy of mithaq in the process lists its seals here, so a step a second install sealed is
|
|
22
|
+
// named rather than miscounted. Anything can write this map, so its answer only ever refuses.
|
|
23
|
+
const copies = (globalThis[Symbol.for('mithaq.copies')] ??= new Map());
|
|
24
|
+
copies.set(import.meta.url, (step) => seals.get(step));
|
|
25
|
+
/** Where the other copy of mithaq that sealed this step was loaded from, if one did. */
|
|
26
|
+
export const foreignCopyOf = (step) => [...copies].find(([url, of]) => url !== import.meta.url && of(step))?.[0];
|
|
20
27
|
/** Hold an operation's `output` function until `build` has the derived shapes. */
|
|
21
28
|
export const holdOutput = (operation, of) => {
|
|
22
29
|
outputs.set(operation, of);
|
|
@@ -51,7 +58,7 @@ export const audit = seal('audit', async (ctx, tools) => {
|
|
|
51
58
|
operation: tools.operation,
|
|
52
59
|
dataBefore: withoutSecrets(ctx.kayan, ctx.dataBefore),
|
|
53
60
|
dataAfter: withoutSecrets(ctx.kayan, ctx.dataAfter ?? snapshot(ctx.data) ?? ctx.data),
|
|
54
|
-
store: 'store' in tools ? tools.store : undefined,
|
|
61
|
+
store: 'store' in tools ? rawStepStore(tools.store) : undefined,
|
|
55
62
|
tx: ctx.tx,
|
|
56
63
|
user: ctx.user,
|
|
57
64
|
});
|
|
@@ -60,9 +67,9 @@ export const audit = seal('audit', async (ctx, tools) => {
|
|
|
60
67
|
// A row's filter and a default read the same reference off the same actor, so they agree.
|
|
61
68
|
const defaultOf = (field, ctx, tools) => {
|
|
62
69
|
const value = field.rules?.defaultValue;
|
|
63
|
-
// Copied, so a step
|
|
70
|
+
// Copied whole, so a step changing one row's list or object never changes the next row's default.
|
|
64
71
|
if (!isRef(value))
|
|
65
|
-
return
|
|
72
|
+
return typeof value === 'object' && value !== null ? structuredClone(value) : value;
|
|
66
73
|
if (!ctx.user) {
|
|
67
74
|
throw new Error(`Kayan "${ctx.kayan.name}" operation "${tools.operation}" field "${field.name}" defaults to "user.${value.$user}", and this write was given no actor. Pass one: create(data, { actor }).`);
|
|
68
75
|
}
|
|
@@ -151,7 +158,7 @@ export const validate = seal('validate', (() => validate));
|
|
|
151
158
|
const columns = (ctx) => Object.fromEntries(Object.entries(ctx.data).filter(([name]) => name in ctx.kayan.fields));
|
|
152
159
|
/** Make the row, and what came back is what the rest of the list sees. */
|
|
153
160
|
export const insert = seal('insert', async (ctx, tools) => {
|
|
154
|
-
const row = await tools.store.insert({ tx: ctx.tx, kayan: ctx.kayan
|
|
161
|
+
const row = await tools.store.insert({ tx: ctx.tx, kayan: ctx.kayan, data: columns(ctx) });
|
|
155
162
|
return { ...ctx, data: { ...ctx.data, ...row }, dataAfter: snapshot(row) };
|
|
156
163
|
});
|
|
157
164
|
/** Change the one row the id names; everything else in `data` is the change. An id alone is refused with a 400. */
|
|
@@ -168,12 +175,12 @@ export const update = seal('update', async (ctx, tools) => {
|
|
|
168
175
|
params: { kayan: ctx.kayan.name, operation: tools.operation },
|
|
169
176
|
});
|
|
170
177
|
}
|
|
171
|
-
const row = await tools.store.update({ tx: ctx.tx, kayan: ctx.kayan
|
|
178
|
+
const row = await tools.store.update({ tx: ctx.tx, kayan: ctx.kayan, id: ctx.data.id, data: values, where: tools.where });
|
|
172
179
|
return { ...ctx, data: { ...ctx.data, ...row }, dataAfter: snapshot(row) };
|
|
173
180
|
});
|
|
174
181
|
/** Remove the one row the id names, and hand back what was removed. */
|
|
175
182
|
export const remove = seal('remove', async (ctx, tools) => {
|
|
176
|
-
const data = await tools.store.remove({ tx: ctx.tx, kayan: ctx.kayan
|
|
183
|
+
const data = await tools.store.remove({ tx: ctx.tx, kayan: ctx.kayan, id: ctx.data.id, where: tools.where });
|
|
177
184
|
return { ...ctx, data, dataAfter: snapshot(data) };
|
|
178
185
|
});
|
|
179
186
|
// The seal and nothing else: `bulk` supplies `tools.many`.
|
|
@@ -254,11 +261,10 @@ export const resolveOperations = (declared, slots = {}) => {
|
|
|
254
261
|
afterSteps: slots.afterSteps ?? [],
|
|
255
262
|
afterCommit: slots.afterCommit ?? [],
|
|
256
263
|
};
|
|
257
|
-
|
|
258
|
-
if (!record)
|
|
264
|
+
if (!declared)
|
|
259
265
|
return {};
|
|
260
266
|
const resolved = Object.create(null);
|
|
261
|
-
for (const [name, declaration] of Object.entries(
|
|
267
|
+
for (const [name, declaration] of Object.entries(declared)) {
|
|
262
268
|
if (declaration === undefined)
|
|
263
269
|
continue;
|
|
264
270
|
const held = declaration;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RawStepStore, StepStore } from '../../mithaq/language.ts';
|
|
2
|
+
import type { RuntimeWriteTools, WriteTools } from './steps.ts';
|
|
3
|
+
export declare const rawStepStore: (store: StepStore) => RawStepStore;
|
|
4
|
+
export declare const stepTools: (tools: RuntimeWriteTools) => WriteTools;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { invalid } from './errors.js';
|
|
2
|
+
// Audit sinks still implement the adapter contract; no raw handle is a property a step can reach.
|
|
3
|
+
const raw = new WeakMap();
|
|
4
|
+
const declaredStore = (store) => {
|
|
5
|
+
const wrapped = Object.freeze(Object.fromEntries(Object.entries(store).map(([word, call]) => [word, (args) => {
|
|
6
|
+
const kayan = args.kayan;
|
|
7
|
+
if (!kayan || typeof kayan !== 'object' || typeof kayan.name !== 'string' || !kayan.fields || typeof kayan.fields !== 'object') {
|
|
8
|
+
throw invalid({ message: 'A step store call names its kayan by its declaration, not a string.', key: 'error.invalid.value' });
|
|
9
|
+
}
|
|
10
|
+
return call({ ...args, kayan: kayan.name });
|
|
11
|
+
}])));
|
|
12
|
+
raw.set(wrapped, store);
|
|
13
|
+
return wrapped;
|
|
14
|
+
};
|
|
15
|
+
export const rawStepStore = (store) => {
|
|
16
|
+
const held = raw.get(store);
|
|
17
|
+
if (!held)
|
|
18
|
+
throw new Error('The audit step needs the store handed to it by the runner.');
|
|
19
|
+
return held;
|
|
20
|
+
};
|
|
21
|
+
export const stepTools = (tools) => Object.freeze({ ...tools, store: declaredStore(tools.store) });
|
|
@@ -2,7 +2,7 @@ import { type AssignedResolver } from '../actor.ts';
|
|
|
2
2
|
import { type Language, type Store } from '../../mithaq/language.ts';
|
|
3
3
|
import type { ResolvedKayan } from '../../mithaq/types.ts';
|
|
4
4
|
import { type Where } from '../../mithaq/where.ts';
|
|
5
|
-
import type { OperationEffect, WriteTools } from './steps.ts';
|
|
5
|
+
import type { OperationEffect, RuntimeWriteTools as WriteTools } from './steps.ts';
|
|
6
6
|
import { type Authorize } from './write.ts';
|
|
7
7
|
/** How far a read reaches past the active organization: every verified membership, or every tenant. */
|
|
8
8
|
export type TenantReach = 'mine' | 'all';
|
|
@@ -47,7 +47,7 @@ export declare const tenancy: ({ language, assigned, onError, collection }: {
|
|
|
47
47
|
scoped: (inner: Authorize) => Authorize;
|
|
48
48
|
active: (actor: unknown) => string | undefined;
|
|
49
49
|
sealWrites: ({ writes, find, kayanOf }: {
|
|
50
|
-
writes: WriteTools['store']
|
|
50
|
+
writes: Omit<WriteTools['store'], 'hold'>;
|
|
51
51
|
find: Store['find'];
|
|
52
52
|
kayanOf: (name: string) => ResolvedKayan | undefined;
|
|
53
53
|
}) => WriteTools['store'];
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// access answers, so no row table, callback or authorizer can widen it.
|
|
3
3
|
import { authActor, markAuthActor, resolveAssigned } from '../actor.js';
|
|
4
4
|
import { isTenant } from '../../mithaq/language.js';
|
|
5
|
-
import { combineWhere, same } from '../../mithaq/where.js';
|
|
6
|
-
import { AsasError, denied, invalid } from './errors.js';
|
|
5
|
+
import { combineWhere, compareValues, same } from '../../mithaq/where.js';
|
|
6
|
+
import { AsasError, denied, invalid, missing } from './errors.js';
|
|
7
7
|
import { running } from './runtime.js';
|
|
8
|
-
import { assertReferences } from './write.js';
|
|
8
|
+
import { assertStoreWhere, assertReferences } from './write.js';
|
|
9
9
|
// The session must come from the one Better Auth kayan of the build, whose organizations the
|
|
10
10
|
// tenant kayan holds: another instance's organization ids name no tenant here.
|
|
11
11
|
const sessionOf = ({ actor, collection }) => {
|
|
@@ -89,8 +89,8 @@ export const tenancy = ({ language, assigned, onError, collection }) => {
|
|
|
89
89
|
}
|
|
90
90
|
if (reach === 'all') {
|
|
91
91
|
const trusted = authActor(actor);
|
|
92
|
-
if (!trusted || !target.
|
|
93
|
-
throw refused('tenant "all" reads every organization, which only
|
|
92
|
+
if (!trusted || !target.backoffice || (await target.backoffice({ user: actor })) !== true) {
|
|
93
|
+
throw refused('tenant "all" reads every organization, which only backoffice staff in the backoffice app may.');
|
|
94
94
|
}
|
|
95
95
|
return { reach };
|
|
96
96
|
}
|
|
@@ -173,13 +173,34 @@ export const tenancy = ({ language, assigned, onError, collection }) => {
|
|
|
173
173
|
* organization's delete never reaches it.
|
|
174
174
|
*/
|
|
175
175
|
const sealWrites = ({ writes, find, kayanOf }) => {
|
|
176
|
-
|
|
176
|
+
// A versions table or a join table holds no tenant field to seal by, so a step reaches neither.
|
|
177
|
+
const named = (name) => {
|
|
177
178
|
const kayan = kayanOf(name);
|
|
178
|
-
if (kayan)
|
|
179
|
-
|
|
179
|
+
if (!kayan)
|
|
180
|
+
throw new Error(`A step wrote "${name}", which names no kayan, and a step writes a kayan's rows alone: versions and join rows are core's.`);
|
|
181
|
+
return kayan;
|
|
182
|
+
};
|
|
183
|
+
const declaredField = (name, key) => {
|
|
184
|
+
if (!Object.hasOwn(named(name).fields, key))
|
|
185
|
+
throw invalid({ message: `Kayan "${name}" declares no field "${key}".`, key: 'error.invalid.value', params: { kayan: name, field: key } });
|
|
186
|
+
};
|
|
187
|
+
const checked = (name, where) => {
|
|
188
|
+
if (!where)
|
|
189
|
+
return where;
|
|
190
|
+
try {
|
|
191
|
+
return assertStoreWhere(named(name), where);
|
|
192
|
+
}
|
|
193
|
+
catch (cause) {
|
|
194
|
+
throw invalid({ message: cause instanceof Error ? cause.message : 'Invalid store filter.', key: 'error.invalid.value', params: { kayan: name }, cause });
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const references = async ({ name, data, tx }) => {
|
|
198
|
+
for (const key of Object.keys(data))
|
|
199
|
+
declaredField(name, key);
|
|
200
|
+
return assertReferences({ kayan: named(name), store: { find }, data, tenant: active(running.getStore()?.user), tx });
|
|
180
201
|
};
|
|
181
202
|
const sealed = (name, data) => {
|
|
182
|
-
const field =
|
|
203
|
+
const field = named(name).tenant;
|
|
183
204
|
if (!field)
|
|
184
205
|
return undefined;
|
|
185
206
|
const org = active(running.getStore()?.user);
|
|
@@ -189,24 +210,65 @@ export const tenancy = ({ language, assigned, onError, collection }) => {
|
|
|
189
210
|
throw new Error(`A step wrote field "${field}" of kayan "${name}", and a row is made in its organization by the runner alone and never leaves it.`);
|
|
190
211
|
return { [field]: { equals: org } };
|
|
191
212
|
};
|
|
213
|
+
const sealedFind = async (args) => {
|
|
214
|
+
if (args.args.lock && !args.tx)
|
|
215
|
+
throw new Error('Holding rows requires a transaction.');
|
|
216
|
+
declaredField(args.kayan, args.args.sort.field);
|
|
217
|
+
return find({ ...args, args: { ...args.args, where: combineWhere(checked(args.kayan, args.args.where), sealed(args.kayan)) } });
|
|
218
|
+
};
|
|
192
219
|
return Object.freeze({
|
|
220
|
+
find: sealedFind,
|
|
221
|
+
sum: async (args) => {
|
|
222
|
+
declaredField(args.kayan, args.field);
|
|
223
|
+
return writes.sum({ ...args, where: combineWhere(checked(args.kayan, args.where), sealed(args.kayan)) });
|
|
224
|
+
},
|
|
225
|
+
hold: async ({ tx, kayan, ids, mode }) => {
|
|
226
|
+
if (!['update', 'noKeyUpdate', 'share', 'keyShare'].includes(mode))
|
|
227
|
+
throw invalid({ message: 'Holding rows requires a valid lock mode.', key: 'error.invalid.value' });
|
|
228
|
+
if (!tx)
|
|
229
|
+
throw new Error('Holding rows requires a transaction.');
|
|
230
|
+
const uuid = named(kayan).fields.id?.kind === 'uuid';
|
|
231
|
+
const ordered = [...new Set(ids.map((id) => uuid && typeof id === 'string' ? id.toLowerCase() : id))].sort((left, right) => compareValues({ left, right, direction: 'asc' }));
|
|
232
|
+
for (const id of ordered) {
|
|
233
|
+
const { docs } = await sealedFind({ tx, kayan, args: { limit: 1, page: 1, sort: { field: 'id', direction: 'asc' }, lock: mode, where: { id: { equals: id } } } });
|
|
234
|
+
if (!docs.length)
|
|
235
|
+
throw missing({ message: `No "${kayan}" row found.`, key: 'error.not_found.row', params: { kayan } });
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
upsert: async (args) => {
|
|
239
|
+
const field = named(args.kayan).tenant;
|
|
240
|
+
const org = active(running.getStore()?.user);
|
|
241
|
+
if (field && (org === undefined || (Object.hasOwn(args.data, field) && args.data[field] !== org))) {
|
|
242
|
+
throw new Error(`A step upserted kayan "${args.kayan}" in another organization than the active one.`);
|
|
243
|
+
}
|
|
244
|
+
const data = field ? { ...args.data, [field]: org } : args.data;
|
|
245
|
+
const kayan = named(args.kayan);
|
|
246
|
+
if (!args.on.length || new Set(args.on).size !== args.on.length || !(args.on.length === 1 && args.on[0] === 'id') && !kayan.indexes.some((index) => index.unique && index.fields.length === args.on.length && index.fields.every((field) => args.on.includes(field))) || args.on.some((key) => !Object.hasOwn(data, key) || (data[key] && typeof data[key] === 'object' && '$inc' in data[key]))) {
|
|
247
|
+
throw invalid({ message: `Kayan "${args.kayan}" upsert needs plain key values for a declared unique index in "on".`, key: 'error.invalid.value', params: { kayan: args.kayan } });
|
|
248
|
+
}
|
|
249
|
+
for (const key of args.on)
|
|
250
|
+
declaredField(args.kayan, key);
|
|
251
|
+
await references({ name: args.kayan, data, tx: args.tx });
|
|
252
|
+
return writes.upsert({ ...args, data, where: combineWhere(checked(args.kayan, args.where), sealed(args.kayan)) });
|
|
253
|
+
},
|
|
193
254
|
insert: async (args) => {
|
|
194
|
-
const field =
|
|
255
|
+
const field = named(args.kayan).tenant;
|
|
256
|
+
const org = active(running.getStore()?.user);
|
|
195
257
|
if (field) {
|
|
196
|
-
|
|
197
|
-
if (org === undefined || args.data[field] !== org) {
|
|
258
|
+
if (org === undefined || (Object.hasOwn(args.data, field) && args.data[field] !== org)) {
|
|
198
259
|
throw new Error(`A step made a row of kayan "${args.kayan}" in another organization than the active one, and a row is made in its own organization alone.`);
|
|
199
260
|
}
|
|
200
261
|
}
|
|
201
|
-
|
|
202
|
-
|
|
262
|
+
const data = field ? { ...args.data, [field]: org } : args.data;
|
|
263
|
+
await references({ name: args.kayan, data, tx: args.tx });
|
|
264
|
+
return writes.insert({ ...args, data });
|
|
203
265
|
},
|
|
204
266
|
update: async (args) => {
|
|
205
|
-
const where = combineWhere(args.where, sealed(args.kayan, args.data));
|
|
267
|
+
const where = combineWhere(checked(args.kayan, args.where), sealed(args.kayan, args.data));
|
|
206
268
|
await references({ name: args.kayan, data: args.data, tx: args.tx });
|
|
207
269
|
return writes.update({ ...args, where });
|
|
208
270
|
},
|
|
209
|
-
remove: (args) => writes.remove({ ...args, where: combineWhere(args.where, sealed(args.kayan)) }),
|
|
271
|
+
remove: async (args) => writes.remove({ ...args, where: combineWhere(checked(args.kayan, args.where), sealed(args.kayan)) }),
|
|
210
272
|
});
|
|
211
273
|
};
|
|
212
274
|
return { open, scoped, active, sealWrites };
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { AdminAccess, LocalesOf, ResolvedKayan, RuntimeField, ServiceOf } from '../../mithaq/types.ts';
|
|
1
|
+
import type { AdminAccess, LocalesOf, ResolvedField, ResolvedKayan, RuntimeField, ServiceOf } from '../../mithaq/types.ts';
|
|
2
2
|
import type { BuiltTables } from '../../drizzle/index.ts';
|
|
3
3
|
import { defaultKinds, type Kind } from '../../base/kinds.ts';
|
|
4
|
-
import type { RichTextValue } from '../../rich-text/
|
|
4
|
+
import type { RichTextValue } from '../../rich-text/value.ts';
|
|
5
5
|
import type { Store } from '../../mithaq/language.ts';
|
|
6
6
|
import type { LanguageOf } from '../../mithaq/define.ts';
|
|
7
7
|
import type { InferInput, InferOutput, StandardSchemaV1 } from '../../mithaq/standard-schema.ts';
|
|
8
|
-
import { type BulkResult, type Context, type ContextOf, type Ctx, type DerivedSchemas, type OperationEffect, type ResolvedOperation, type StepsOf, type Tools, type WriteTools, type WriteStep } from './steps.ts';
|
|
8
|
+
import { type BulkResult, type Context, type ContextOf, type Ctx, type DerivedSchemas, type OperationEffect, type ResolvedOperation, type StepsOf, type Tools, type RuntimeWriteTools as WriteTools, type WriteStep } from './steps.ts';
|
|
9
9
|
import { type Open } from './runtime.ts';
|
|
10
10
|
import type { FindInput } from './read.ts';
|
|
11
11
|
import { type TenantReach } from './tenant.ts';
|
|
12
|
-
import type { FieldWhere, Operator, Where } from '../../mithaq/where.ts';
|
|
12
|
+
import type { FieldWhere, Operator, Scalar, Where } from '../../mithaq/where.ts';
|
|
13
13
|
/** The key of `Options` only the bulk runner sets. */
|
|
14
14
|
export declare const ontoRow: unique symbol;
|
|
15
15
|
/** The caller's own context, required where the project declared one. */
|
|
@@ -32,7 +32,7 @@ export type ReadOptions<Actor = unknown, Extra extends Record<string, unknown> =
|
|
|
32
32
|
/**
|
|
33
33
|
* How far the read reaches past the active organization: `'mine'` is every organization the
|
|
34
34
|
* actor is a verified member of, each under the roles it holds there; `'all'` is every tenant,
|
|
35
|
-
* for
|
|
35
|
+
* for backoffice staff in the backoffice app alone. A `where` on the tenant field only narrows it.
|
|
36
36
|
*/
|
|
37
37
|
tenant?: TenantReach;
|
|
38
38
|
};
|
|
@@ -49,6 +49,8 @@ export type ManagedAdmin<Actor = unknown> = {
|
|
|
49
49
|
access?: AdminAccess<Actor>;
|
|
50
50
|
kayan?: string;
|
|
51
51
|
};
|
|
52
|
+
/** Raw store filters keep their adapter's value and empty-filter semantics; only declared paths are checked here. */
|
|
53
|
+
export declare const assertStoreWhere: (kayan: ResolvedKayan, where: Where) => Where;
|
|
52
54
|
export declare const assertFieldWhere: ({ said, key, kind, operators, value, valued }: {
|
|
53
55
|
said: string;
|
|
54
56
|
key: string;
|
|
@@ -59,8 +61,37 @@ export declare const assertFieldWhere: ({ said, key, kind, operators, value, val
|
|
|
59
61
|
}) => FieldWhere;
|
|
60
62
|
export declare const assertWhere: (kayan: ResolvedKayan, where: unknown) => Where;
|
|
61
63
|
export declare const assertWhereGrammar: (kayan: ResolvedKayan, where: unknown) => Where;
|
|
64
|
+
/**
|
|
65
|
+
* How large a caller's `where` may be: the values one `in` or `not_in` lists, and the fields, `and`s and
|
|
66
|
+
* `or`s it holds in all, however nested. Each is a condition the store runs, so past these a read is
|
|
67
|
+
* refused before it reaches the store rather than holding it for minutes.
|
|
68
|
+
*/
|
|
69
|
+
export declare const whereLimits: {
|
|
70
|
+
readonly values: 1000;
|
|
71
|
+
readonly nodes: 200;
|
|
72
|
+
};
|
|
62
73
|
export declare const aliveWhere: (kayan: ResolvedKayan) => Where | undefined;
|
|
63
74
|
export declare const publishedWhere: (kayan: ResolvedKayan) => Where | undefined;
|
|
75
|
+
/** A field of one kayan that points at rows of another, which a delete of one of those rows answers to. */
|
|
76
|
+
export type Pointer = {
|
|
77
|
+
kayan: ResolvedKayan;
|
|
78
|
+
field: ResolvedField;
|
|
79
|
+
};
|
|
80
|
+
/** Every field of `kayans` whose `onDelete` keeps a row of `target` while it names it. */
|
|
81
|
+
export declare const restrictingPointers: (kayans: readonly ResolvedKayan[], target: ResolvedKayan) => Pointer[];
|
|
82
|
+
/**
|
|
83
|
+
* The ids of the rows of each kayan that name `id` through `pointers`, a row whose latest version
|
|
84
|
+
* names it among them, since the foreign key holds live rows alone and a pending draft would
|
|
85
|
+
* otherwise keep an id nothing answers for. A pointing kayan that is tenant-owned is read within
|
|
86
|
+
* `tenant` when one is given, so a count never reaches another organization's rows.
|
|
87
|
+
*/
|
|
88
|
+
export declare const referencingIds: ({ id, pointers, store, tenant, tx }: {
|
|
89
|
+
id: unknown;
|
|
90
|
+
pointers: readonly Pointer[];
|
|
91
|
+
store: Pick<Store, 'find'>;
|
|
92
|
+
tenant?: unknown;
|
|
93
|
+
tx?: unknown;
|
|
94
|
+
}) => Promise<Map<ResolvedKayan, Set<string>>>;
|
|
64
95
|
export declare const projectReadableFields: <Actor>({ kayan, row, actor, }: {
|
|
65
96
|
kayan: ResolvedKayan;
|
|
66
97
|
row: Record<string, unknown>;
|
|
@@ -77,11 +108,24 @@ export declare const publicResult: <Actor>({ kayan, row, actor, internalOutput,
|
|
|
77
108
|
}) => Promise<Record<string, unknown>>;
|
|
78
109
|
export declare const assertSingleConstrained: (kayan: ResolvedKayan, where: Where | undefined) => void;
|
|
79
110
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
111
|
+
* The lock a write holds on a row it changes, as Postgres's own statement takes it: `FOR UPDATE` for a
|
|
112
|
+
* delete, or for an update naming a column of a unique index, which holds off a write naming the row
|
|
113
|
+
* `FOR KEY SHARE`, and `FOR NO KEY UPDATE` for any other update, which that write passes. A soft delete
|
|
114
|
+
* is an update of its mark, and asks no rows naming its row, so it has no reference to hold off. Taken
|
|
115
|
+
* whole when the row loads, before any value is compared: an UPDATE raising its lock later would wait on
|
|
116
|
+
* a writer that named the row in between while that writer waits on it, a deadlock.
|
|
117
|
+
*
|
|
118
|
+
* ponytail: publishing a latest draft's unique column can still raise this lock; take `update` for every
|
|
119
|
+
* non-draft update on a kayan with drafts and a unique index if that path needs to be covered.
|
|
120
|
+
* ponytail: an update step can raise its own row's lock by writing a unique column or hard-deleting the
|
|
121
|
+
* row; take `update` at load for an operation whose steps may do either if that path needs to be covered.
|
|
83
122
|
*/
|
|
84
|
-
export declare const
|
|
123
|
+
export declare const rowLock: ({ kayan, kind, data }: {
|
|
124
|
+
kayan: ResolvedKayan;
|
|
125
|
+
kind: string;
|
|
126
|
+
data: Record<string, unknown>;
|
|
127
|
+
}) => "noKeyUpdate" | "update";
|
|
128
|
+
export declare const assertReferences: ({ kayan, store, data, tenant, access, offered, tx }: {
|
|
85
129
|
kayan: ResolvedKayan;
|
|
86
130
|
store: Pick<Store, 'find'>;
|
|
87
131
|
data: Record<string, unknown>;
|
|
@@ -89,6 +133,8 @@ export declare const assertReferences: ({ kayan, store, data, tenant, access, tx
|
|
|
89
133
|
tenant: unknown;
|
|
90
134
|
/** What else the target's row must meet: its read access for this actor. */
|
|
91
135
|
access?: (target: ResolvedKayan) => Promise<Where | undefined>;
|
|
136
|
+
/** What one field's `filterOptions` offers this actor, which a managed write is held to. */
|
|
137
|
+
offered?: (field: ResolvedField) => Promise<Where | undefined>;
|
|
92
138
|
tx?: unknown;
|
|
93
139
|
}) => Promise<void>;
|
|
94
140
|
export declare const defaultAccess: <Actor>(admin: ManagedAdmin<Actor> | undefined, actor: Actor | null) => Promise<boolean>;
|
|
@@ -126,7 +172,7 @@ export declare const runOutside: <Actor>({ kayan, tools, slot, list, start, arou
|
|
|
126
172
|
};
|
|
127
173
|
}) => Promise<Ctx>;
|
|
128
174
|
export declare const drain: (queue: readonly (() => Promise<void>)[]) => Promise<void>;
|
|
129
|
-
export declare const write: <Actor = unknown>({ kayan, declaration, tools, store, cap, authorize, admin, tenantOf, }: {
|
|
175
|
+
export declare const write: <Actor = unknown>({ kayan, declaration, tools, store, cap, authorize, admin, tenantOf, pointers, }: {
|
|
130
176
|
kayan: ResolvedKayan;
|
|
131
177
|
declaration: ResolvedOperation;
|
|
132
178
|
tools: WriteTools<unknown, Actor>;
|
|
@@ -136,6 +182,8 @@ export declare const write: <Actor = unknown>({ kayan, declaration, tools, store
|
|
|
136
182
|
admin?: ManagedAdmin<Actor>;
|
|
137
183
|
/** The organization the actor's session names, which a tenant-owned row is made in. */
|
|
138
184
|
tenantOf: (actor: unknown) => string | undefined;
|
|
185
|
+
/** The fields that keep a row of this kayan while they name it, which a delete answers to. */
|
|
186
|
+
pointers?: readonly Pointer[];
|
|
139
187
|
}) => (data?: Record<string, unknown>, options?: Options<Actor>) => Promise<Record<string, unknown>>;
|
|
140
188
|
type Registry = readonly {
|
|
141
189
|
name: string;
|
|
@@ -354,6 +402,120 @@ type InputOf<K, Name extends PropertyKey, Fields extends Record<string, RuntimeF
|
|
|
354
402
|
type BoundOne<K, Fields extends Record<string, RuntimeField>, L extends LanguageValues, List extends Registry, Mode extends 'read' | 'write'> = {
|
|
355
403
|
[Name in keyof OwnOperations<K> as Mode extends 'write' ? Name : IsReadOperation<OwnOperation<K, Name>> extends true ? Name : never]: (...input: {} extends InputOf<K, Name, Fields, L, List> ? [input?: InputOf<K, Name, Fields, L, List>] : [input: InputOf<K, Name, Fields, L, List>]) => OperationResult<OwnOperation<K, Name>, Fields, L, List>;
|
|
356
404
|
};
|
|
405
|
+
type StoreFields<K extends Registry[number]> = {
|
|
406
|
+
[N in keyof K['fields'] as K['fields'][N] extends {
|
|
407
|
+
kind: 'join';
|
|
408
|
+
} ? never : N]: K['fields'][N];
|
|
409
|
+
};
|
|
410
|
+
type StoreNames<K extends Registry[number]> = keyof StoreFields<K> & string;
|
|
411
|
+
type StoreNumeric<K extends Registry[number]> = string extends StoreNames<K> ? string : {
|
|
412
|
+
[N in StoreNames<K>]: K['fields'][N] extends {
|
|
413
|
+
kind: 'integer' | 'number' | 'decimal';
|
|
414
|
+
} ? N : never;
|
|
415
|
+
}[StoreNames<K>];
|
|
416
|
+
type StoreValue<K extends Registry[number], F> = ([Value<F, LanguageOf<K>, readonly [K]>] extends [never] ? F extends {
|
|
417
|
+
relationTo: string;
|
|
418
|
+
} ? F extends {
|
|
419
|
+
hasMany: true;
|
|
420
|
+
} ? (string | number)[] : string | number : never : Value<F, LanguageOf<K>, readonly [K]>) | (F extends {
|
|
421
|
+
rules: {
|
|
422
|
+
required: true;
|
|
423
|
+
};
|
|
424
|
+
} ? never : null);
|
|
425
|
+
type StoreData<K extends Registry[number], Increment extends boolean = false> = string extends StoreNames<K> ? Record<string, unknown> : {
|
|
426
|
+
[N in StoreNames<K>]?: (N extends 'id' ? StoreID<K> : StoreValue<K, K['fields'][N]>) | (Increment extends true ? K['fields'][N] extends {
|
|
427
|
+
kind: 'integer';
|
|
428
|
+
} ? {
|
|
429
|
+
$inc: number;
|
|
430
|
+
} : never : never);
|
|
431
|
+
};
|
|
432
|
+
type StorePath<K extends Registry[number]> = StoreNames<K> | {
|
|
433
|
+
[N in StoreNames<K>]: K['fields'][N] extends {
|
|
434
|
+
rules: {
|
|
435
|
+
localized: true;
|
|
436
|
+
};
|
|
437
|
+
} ? `${N}.${LocalesOf<LanguageOf<K>>}` : K['fields'][N] extends {
|
|
438
|
+
relationTo: () => infer Related extends Registry[number];
|
|
439
|
+
} ? `${N}.${keyof Related['fields'] & string}` : K['fields'][N] extends {
|
|
440
|
+
kind: 'json';
|
|
441
|
+
} ? `${N}.${string}` : never;
|
|
442
|
+
}[StoreNames<K>];
|
|
443
|
+
type StorePathValue<K extends Registry[number], Path extends string> = Path extends keyof K['fields'] ? StoreValue<K, K['fields'][Path]> : Path extends `${infer Name}.${infer Rest}` ? Name extends keyof K['fields'] ? K['fields'][Name] extends {
|
|
444
|
+
relationTo: () => infer Related extends Registry[number];
|
|
445
|
+
} ? Rest extends keyof Related['fields'] ? StoreValue<Related, Related['fields'][Rest]> : never : K['fields'][Name] extends {
|
|
446
|
+
rules: {
|
|
447
|
+
localized: true;
|
|
448
|
+
};
|
|
449
|
+
} ? KindCarried<K['fields'][Name], LanguageOf<K>, readonly [K]> | null : Scalar : never : never;
|
|
450
|
+
type StoreCondition<Value> = {
|
|
451
|
+
[O in Operator]?: O extends 'exists' ? boolean : O extends 'in' | 'not_in' ? readonly Value[] : O extends 'like' ? Extract<Value, string> : O extends 'contains' ? Extract<Value, string> | (Value extends readonly (infer Item)[] ? Item : never) : Value;
|
|
452
|
+
};
|
|
453
|
+
type StoreWhere<K extends Registry[number]> = string extends StoreNames<K> ? Where : {
|
|
454
|
+
[P in StorePath<K>]?: StoreCondition<StorePathValue<K, P>>;
|
|
455
|
+
} & {
|
|
456
|
+
and?: readonly StoreWhere<K>[];
|
|
457
|
+
or?: readonly StoreWhere<K>[];
|
|
458
|
+
};
|
|
459
|
+
type StoreResult<K extends Registry[number]> = StoredRow<StoreFields<K>, LanguageOf<K>, readonly [K]>;
|
|
460
|
+
type StoreID<K extends Registry[number]> = string extends StoreNames<K> ? unknown : Exclude<Value<K['fields']['id'], LanguageOf<K>, readonly [K]>, null | undefined>;
|
|
461
|
+
type StoreLock = NonNullable<Parameters<Store['find']>[0]['args']['lock']>;
|
|
462
|
+
/** The declaration carries the language and fields, so a step needs no separate name registry. */
|
|
463
|
+
export type DeclarationStore = {
|
|
464
|
+
insert<const K extends Registry[number]>(args: {
|
|
465
|
+
tx: unknown;
|
|
466
|
+
kayan: K;
|
|
467
|
+
data: StoreData<NoInfer<K>>;
|
|
468
|
+
}): Promise<StoreResult<K>>;
|
|
469
|
+
update<const K extends Registry[number]>(args: {
|
|
470
|
+
tx: unknown;
|
|
471
|
+
kayan: K;
|
|
472
|
+
id: StoreID<NoInfer<K>>;
|
|
473
|
+
data: StoreData<NoInfer<K>, true>;
|
|
474
|
+
where?: StoreWhere<NoInfer<K>>;
|
|
475
|
+
}): Promise<StoreResult<K>>;
|
|
476
|
+
upsert<const K extends Registry[number]>(args: {
|
|
477
|
+
tx: unknown;
|
|
478
|
+
kayan: K;
|
|
479
|
+
on: readonly StoreNames<NoInfer<K>>[];
|
|
480
|
+
data: StoreData<NoInfer<K>, true>;
|
|
481
|
+
where?: StoreWhere<NoInfer<K>>;
|
|
482
|
+
}): Promise<StoreResult<K>>;
|
|
483
|
+
remove<const K extends Registry[number]>(args: {
|
|
484
|
+
tx: unknown;
|
|
485
|
+
kayan: K;
|
|
486
|
+
id: StoreID<NoInfer<K>>;
|
|
487
|
+
where?: StoreWhere<NoInfer<K>>;
|
|
488
|
+
}): Promise<StoreResult<K>>;
|
|
489
|
+
find<const K extends Registry[number]>(args: {
|
|
490
|
+
tx?: unknown;
|
|
491
|
+
kayan: K;
|
|
492
|
+
args: {
|
|
493
|
+
limit: number;
|
|
494
|
+
page: number;
|
|
495
|
+
sort: {
|
|
496
|
+
field: StoreNames<NoInfer<K>>;
|
|
497
|
+
direction: 'asc' | 'desc';
|
|
498
|
+
};
|
|
499
|
+
lock?: StoreLock;
|
|
500
|
+
where?: StoreWhere<NoInfer<K>>;
|
|
501
|
+
};
|
|
502
|
+
}): Promise<{
|
|
503
|
+
docs: StoreResult<K>[];
|
|
504
|
+
totalDocs: number;
|
|
505
|
+
}>;
|
|
506
|
+
hold<const K extends Registry[number]>(args: {
|
|
507
|
+
tx: unknown;
|
|
508
|
+
kayan: K;
|
|
509
|
+
ids: readonly StoreID<NoInfer<K>>[];
|
|
510
|
+
mode: StoreLock;
|
|
511
|
+
}): Promise<void>;
|
|
512
|
+
sum<const K extends Registry[number]>(args: {
|
|
513
|
+
tx?: unknown;
|
|
514
|
+
kayan: K;
|
|
515
|
+
field: StoreNumeric<NoInfer<K>>;
|
|
516
|
+
where?: StoreWhere<NoInfer<K>>;
|
|
517
|
+
}): Promise<string>;
|
|
518
|
+
};
|
|
357
519
|
export type BoundOf<Mode extends 'read' | 'write'> = <const K extends Registry[number] & {
|
|
358
520
|
single?: never;
|
|
359
521
|
}>(kayan: K) => BoundOne<K, K['fields'], LanguageOf<K>, readonly [K], Mode>;
|
|
@@ -412,7 +574,7 @@ type GlobalOperations<K extends Registry[number], List extends Registry, L exten
|
|
|
412
574
|
findOne(...args: CallArgs<L, Actor, {
|
|
413
575
|
depth?: number;
|
|
414
576
|
}>): Promise<Row<K['fields'], L, List> | null>;
|
|
415
|
-
update(...args: CallArgs<L, Actor, UpdateInput<K['fields'], L, List>>): Promise<Row<K['fields'], L, List>>;
|
|
577
|
+
update(...args: CallArgs<L, Actor, Omit<OperationData<OwnOperation<K, 'update'>, UpdateInput<K['fields'], L, List>>, 'id'>>): Promise<Row<K['fields'], L, List>>;
|
|
416
578
|
};
|
|
417
579
|
/** What `asas.global` holds: one built global per name, of the shape a kayan has. */
|
|
418
580
|
export type Globals<GlobalList extends Registry, List extends Registry, L extends LanguageValues, Actor = unknown> = {
|