@drhomet/mithaq 0.0.0-stage → 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/LICENSE +22 -0
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/README.md +160 -2
- package/dist/asas/actor.d.ts +96 -0
- package/dist/asas/actor.js +114 -0
- package/dist/asas/build.d.ts +239 -0
- package/dist/asas/build.js +834 -0
- package/dist/asas/cli/mithaq.d.ts +2 -0
- package/dist/asas/cli/mithaq.js +177 -0
- package/dist/asas/cli/upgrade.d.ts +33 -0
- package/dist/asas/cli/upgrade.js +256 -0
- package/dist/asas/http/fetch.d.ts +41 -0
- package/dist/asas/http/fetch.js +178 -0
- package/dist/asas/http/orpc.d.ts +45 -0
- package/dist/asas/http/orpc.js +153 -0
- package/dist/asas/runner/bulk.d.ts +20 -0
- package/dist/asas/runner/bulk.js +196 -0
- package/dist/asas/runner/errors.d.ts +63 -0
- package/dist/asas/runner/errors.js +75 -0
- package/dist/asas/runner/read.d.ts +72 -0
- package/dist/asas/runner/read.js +601 -0
- package/dist/asas/runner/runtime.d.ts +21 -0
- package/dist/asas/runner/runtime.js +8 -0
- package/dist/asas/runner/steps.d.ts +418 -0
- package/dist/asas/runner/steps.js +277 -0
- package/dist/asas/runner/store.d.ts +4 -0
- package/dist/asas/runner/store.js +21 -0
- package/dist/asas/runner/tenant.d.ts +54 -0
- package/dist/asas/runner/tenant.js +275 -0
- package/dist/asas/runner/write.d.ts +606 -0
- package/dist/asas/runner/write.js +1019 -0
- package/dist/auth/better-auth/access.d.ts +29 -0
- package/dist/auth/better-auth/access.js +103 -0
- package/dist/auth/better-auth/generate.d.ts +31 -0
- package/dist/auth/better-auth/generate.js +328 -0
- package/dist/auth/better-auth/index.d.ts +94 -0
- package/dist/auth/better-auth/index.js +81 -0
- package/dist/auth/better-auth/tenant.d.ts +26 -0
- package/dist/auth/better-auth/tenant.js +34 -0
- package/dist/auth/local/index.d.ts +285 -0
- package/dist/auth/local/index.js +738 -0
- package/dist/base/decimal.d.ts +9 -0
- package/dist/base/decimal.js +16 -0
- package/dist/base/fields.d.ts +124 -0
- package/dist/base/fields.js +52 -0
- package/dist/base/index.d.ts +47 -0
- package/dist/base/index.js +5 -0
- package/dist/base/json-schema.d.ts +7 -0
- package/dist/base/json-schema.js +35 -0
- package/dist/base/kinds.d.ts +75 -0
- package/dist/base/kinds.js +123 -0
- package/dist/base/rules.d.ts +73 -0
- package/dist/base/rules.js +81 -0
- package/dist/drizzle/index.d.ts +308 -0
- package/dist/drizzle/index.js +951 -0
- package/dist/drizzle/kinds.d.ts +28 -0
- package/dist/drizzle/kinds.js +43 -0
- package/dist/drizzle/migrate.d.ts +159 -0
- package/dist/drizzle/migrate.js +1328 -0
- package/dist/drizzle/proxy/index.d.ts +1 -0
- package/dist/drizzle/proxy/index.js +1 -0
- package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
- package/dist/drizzle/proxy/node-postgres.js +1 -0
- package/dist/drizzle/proxy/pg-core.d.ts +1 -0
- package/dist/drizzle/proxy/pg-core.js +1 -0
- package/dist/drizzle/rules.d.ts +336 -0
- package/dist/drizzle/rules.js +44 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +26 -0
- package/dist/mithaq/define.d.ts +335 -0
- package/dist/mithaq/define.js +57 -0
- package/dist/mithaq/generate-messages.d.ts +38 -0
- package/dist/mithaq/generate-messages.js +235 -0
- package/dist/mithaq/language.d.ts +374 -0
- package/dist/mithaq/language.js +76 -0
- package/dist/mithaq/lock.d.ts +71 -0
- package/dist/mithaq/lock.js +329 -0
- package/dist/mithaq/message-file.d.ts +13 -0
- package/dist/mithaq/message-file.js +29 -0
- package/dist/mithaq/messages.d.ts +55 -0
- package/dist/mithaq/messages.js +111 -0
- package/dist/mithaq/refusal.d.ts +6 -0
- package/dist/mithaq/refusal.js +6 -0
- package/dist/mithaq/resolve.d.ts +10 -0
- package/dist/mithaq/resolve.js +1014 -0
- package/dist/mithaq/rows.d.ts +87 -0
- package/dist/mithaq/rows.js +174 -0
- package/dist/mithaq/rukn.d.ts +72 -0
- package/dist/mithaq/rukn.js +15 -0
- package/dist/mithaq/standard-schema.d.ts +38 -0
- package/dist/mithaq/standard-schema.js +2 -0
- package/dist/mithaq/types.d.ts +386 -0
- package/dist/mithaq/types.js +15 -0
- package/dist/mithaq/user.d.ts +19 -0
- package/dist/mithaq/user.js +16 -0
- package/dist/mithaq/where.d.ts +32 -0
- package/dist/mithaq/where.js +87 -0
- package/dist/rbac/index.d.ts +105 -0
- package/dist/rbac/index.js +182 -0
- package/dist/rbac/steps.d.ts +22 -0
- package/dist/rbac/steps.js +161 -0
- package/dist/rich-text/index.d.ts +28 -0
- package/dist/rich-text/index.js +59 -0
- package/dist/rich-text/rich-text.d.ts +106 -0
- package/dist/rich-text/rich-text.js +249 -0
- package/dist/rich-text/value.d.ts +55 -0
- package/dist/rich-text/value.js +126 -0
- package/dist/screen/client.d.ts +156 -0
- package/dist/screen/client.js +71 -0
- package/dist/screen/generate.d.ts +14 -0
- package/dist/screen/generate.js +268 -0
- package/dist/screen/index.d.ts +496 -0
- package/dist/screen/index.js +663 -0
- package/dist/screen/react.d.ts +97 -0
- package/dist/screen/react.js +58 -0
- package/dist/screen/studio.d.ts +24 -0
- package/dist/screen/studio.js +68 -0
- package/dist/service/index.d.ts +63 -0
- package/dist/service/index.js +15 -0
- package/dist/storage/allowed-type.d.ts +5 -0
- package/dist/storage/allowed-type.js +7 -0
- package/dist/storage/index.d.ts +42 -0
- package/dist/storage/index.js +43 -0
- package/dist/storage/storage.d.ts +386 -0
- package/dist/storage/storage.js +412 -0
- package/dist/targets.d.ts +14 -0
- package/dist/targets.js +16 -0
- package/dist/versions/index.d.ts +37 -0
- package/dist/versions/index.js +8 -0
- package/dist/versions/versions.d.ts +104 -0
- package/dist/versions/versions.js +160 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +81 -0
- package/dist/zod/index.d.ts +193 -0
- package/dist/zod/index.js +122 -0
- package/dist/zod/kinds.d.ts +36 -0
- package/dist/zod/kinds.js +58 -0
- package/dist/zod/rules.d.ts +117 -0
- package/dist/zod/rules.js +67 -0
- 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 +92 -4
- 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
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { AccessReport, AnyRouter, ClientKayan, FetchClient, NavEntry, Row, ScreenClient, Translate } from './client.ts';
|
|
2
|
+
/** Copy in the viewer's locale, and the direction that locale is written in. */
|
|
3
|
+
export type LocaleState = {
|
|
4
|
+
locale: string;
|
|
5
|
+
dir: 'ltr' | 'rtl';
|
|
6
|
+
t: Translate;
|
|
7
|
+
};
|
|
8
|
+
/** What a screen's host provides around every page it draws, and what each hook below reads one part of. */
|
|
9
|
+
export type ScreenState = LocaleState & {
|
|
10
|
+
screen: ScreenClient;
|
|
11
|
+
/** Who is signed in, and the screens of another app the host links for this actor, each a whole URL. */
|
|
12
|
+
user: {
|
|
13
|
+
/** The signed-in person's own id, which a page compares with an id a row names, such as the actor of an audit entry. */
|
|
14
|
+
id?: string;
|
|
15
|
+
name: string;
|
|
16
|
+
email: string;
|
|
17
|
+
screens?: readonly {
|
|
18
|
+
name: string;
|
|
19
|
+
path: string;
|
|
20
|
+
}[];
|
|
21
|
+
/** `names` is each locale's name, the JSON the project's auth setup writes; `name` is the default locale's. */
|
|
22
|
+
organizations?: readonly {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
slug?: string;
|
|
26
|
+
names?: string;
|
|
27
|
+
}[];
|
|
28
|
+
activeOrganizationId?: string | null;
|
|
29
|
+
};
|
|
30
|
+
access: AccessReport;
|
|
31
|
+
client: FetchClient;
|
|
32
|
+
kayan?: ClientKayan;
|
|
33
|
+
record?: Row | null;
|
|
34
|
+
/** The page's path joined to the screen's, set on a page of the screen's own. */
|
|
35
|
+
path?: string;
|
|
36
|
+
};
|
|
37
|
+
/** The form a component sits in. */
|
|
38
|
+
export type FormState = {
|
|
39
|
+
/** The values the form holds now. */
|
|
40
|
+
getData: () => Readonly<Row>;
|
|
41
|
+
/** Changes one field. A field set here is saved even where the page names no leaf for it. */
|
|
42
|
+
setValue: (path: string, value: unknown) => void;
|
|
43
|
+
/** Whether the form is busy saving, when nothing may change. */
|
|
44
|
+
disabled: boolean;
|
|
45
|
+
/** How many saves have landed since the form was seeded. A draft save never bumps the row's `updatedAt`, so watch this instead. */
|
|
46
|
+
saves: number;
|
|
47
|
+
/** The operations beside `update` that take the `expectedUpdatedAt` step's key. */
|
|
48
|
+
expectedUpdatedAtOn?: readonly string[];
|
|
49
|
+
};
|
|
50
|
+
/** What a list hands whatever draws it: one page of rows, and where a row and a new row go. */
|
|
51
|
+
export type ListState = {
|
|
52
|
+
page: {
|
|
53
|
+
docs: readonly Row[];
|
|
54
|
+
totalDocs: number;
|
|
55
|
+
page: number;
|
|
56
|
+
totalPages: number;
|
|
57
|
+
};
|
|
58
|
+
/** Whether this actor may create a row, off the report. */
|
|
59
|
+
creates?: boolean;
|
|
60
|
+
href: (id: string) => string;
|
|
61
|
+
newHref: string;
|
|
62
|
+
};
|
|
63
|
+
/** The one context a screen's host provides. */
|
|
64
|
+
export declare const ScreenContext: import("react").Context<ScreenState | null>;
|
|
65
|
+
/** The form a component sits in, which each form item provides around the page it draws. */
|
|
66
|
+
export declare const FormContext: import("react").Context<FormState | null>;
|
|
67
|
+
/** The list a component draws, which the host provides around a list node. */
|
|
68
|
+
export declare const ListContext: import("react").Context<ListState | null>;
|
|
69
|
+
/** The screen as this actor may see it. */
|
|
70
|
+
export declare const useScreen: () => ScreenClient;
|
|
71
|
+
/** The screen's nav, already cut to the entries this actor may open (`navOf`). */
|
|
72
|
+
export declare const useNav: () => NavEntry[];
|
|
73
|
+
/** Who is signed in. */
|
|
74
|
+
export declare const useUser: () => ScreenState['user'];
|
|
75
|
+
/** What the access report lets this actor do, cut to the screen's kayan. */
|
|
76
|
+
export declare const useAccess: () => AccessReport;
|
|
77
|
+
/** The viewer's locale, its direction and the copy in it. */
|
|
78
|
+
export declare const useLocale: () => LocaleState;
|
|
79
|
+
/** The client posting this screen's operations: `useClient<typeof asas.router>()` types each call. */
|
|
80
|
+
export declare const useClient: <R = AnyRouter>() => FetchClient<R>;
|
|
81
|
+
/** The row the page draws, and `null` where it draws none. */
|
|
82
|
+
export declare const useRecord: () => Row | null;
|
|
83
|
+
/** The kayan the page belongs to. */
|
|
84
|
+
export declare const useKayan: () => ClientKayan;
|
|
85
|
+
/** The form a component sits in; throws outside one. */
|
|
86
|
+
export declare const useForm: () => FormState;
|
|
87
|
+
/** One field of the form a component sits in, by its path: `path`, `value`, `setValue` and `disabled`. */
|
|
88
|
+
export declare const useField: <Value = unknown>({ path }: {
|
|
89
|
+
path: string;
|
|
90
|
+
}) => {
|
|
91
|
+
path: string;
|
|
92
|
+
value: Value;
|
|
93
|
+
setValue: (value: unknown) => void;
|
|
94
|
+
disabled: boolean;
|
|
95
|
+
};
|
|
96
|
+
/** The list a component draws in place of the registry's list. */
|
|
97
|
+
export declare const useList: () => ListState;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Apart from `mithaq/admin`, so that entry, the CLI and a host of another framework load no React.
|
|
2
|
+
import { createContext, use } from 'react';
|
|
3
|
+
/** The one context a screen's host provides. */
|
|
4
|
+
export const ScreenContext = createContext(null);
|
|
5
|
+
/** The form a component sits in, which each form item provides around the page it draws. */
|
|
6
|
+
export const FormContext = createContext(null);
|
|
7
|
+
/** The list a component draws, which the host provides around a list node. */
|
|
8
|
+
export const ListContext = createContext(null);
|
|
9
|
+
const screenState = (hook) => {
|
|
10
|
+
const state = use(ScreenContext);
|
|
11
|
+
if (!state)
|
|
12
|
+
throw new Error(`${hook}() reads what a screen's host provides, and nothing above this component provides it: draw it inside a screen's page.`);
|
|
13
|
+
return state;
|
|
14
|
+
};
|
|
15
|
+
/** The screen as this actor may see it. */
|
|
16
|
+
export const useScreen = () => screenState('useScreen').screen;
|
|
17
|
+
/** The screen's nav, already cut to the entries this actor may open (`navOf`). */
|
|
18
|
+
export const useNav = () => screenState('useNav').screen.nav;
|
|
19
|
+
/** Who is signed in. */
|
|
20
|
+
export const useUser = () => screenState('useUser').user;
|
|
21
|
+
/** What the access report lets this actor do, cut to the screen's kayan. */
|
|
22
|
+
export const useAccess = () => screenState('useAccess').access;
|
|
23
|
+
/** The viewer's locale, its direction and the copy in it. */
|
|
24
|
+
export const useLocale = () => {
|
|
25
|
+
const { locale, dir, t } = screenState('useLocale');
|
|
26
|
+
return { locale, dir, t };
|
|
27
|
+
};
|
|
28
|
+
/** The client posting this screen's operations: `useClient<typeof asas.router>()` types each call. */
|
|
29
|
+
export const useClient = () => screenState('useClient').client;
|
|
30
|
+
/** The row the page draws, and `null` where it draws none. */
|
|
31
|
+
export const useRecord = () => screenState('useRecord').record ?? null;
|
|
32
|
+
/** The kayan the page belongs to. */
|
|
33
|
+
export const useKayan = () => {
|
|
34
|
+
const kayan = screenState('useKayan').kayan;
|
|
35
|
+
if (!kayan)
|
|
36
|
+
throw new Error('useKayan() reads the kayan a page belongs to, and this page belongs to none: a page of the screen\'s own has no kayan.');
|
|
37
|
+
return kayan;
|
|
38
|
+
};
|
|
39
|
+
/** The form a component sits in; throws outside one. */
|
|
40
|
+
export const useForm = () => {
|
|
41
|
+
const form = use(FormContext);
|
|
42
|
+
if (!form) {
|
|
43
|
+
throw new Error('useForm() reads the form a component sits in, and this component sits in none: only a component a form draws, such as one a form node names, can read or write its values.');
|
|
44
|
+
}
|
|
45
|
+
return form;
|
|
46
|
+
};
|
|
47
|
+
/** One field of the form a component sits in, by its path: `path`, `value`, `setValue` and `disabled`. */
|
|
48
|
+
export const useField = ({ path }) => {
|
|
49
|
+
const form = useForm();
|
|
50
|
+
return { path, value: form.getData()[path], setValue: (value) => form.setValue(path, value), disabled: form.disabled };
|
|
51
|
+
};
|
|
52
|
+
/** The list a component draws in place of the registry's list. */
|
|
53
|
+
export const useList = () => {
|
|
54
|
+
const list = use(ListContext);
|
|
55
|
+
if (!list)
|
|
56
|
+
throw new Error('useList() reads the list a component draws, and this component draws none: only a component a list node names can read its rows.');
|
|
57
|
+
return list;
|
|
58
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LeafClient, NodeClient, RegionsClient, ScreenClient } from './index.ts';
|
|
2
|
+
/** One line of the tree, and the lines under it. */
|
|
3
|
+
export type StudioNode = {
|
|
4
|
+
label: string;
|
|
5
|
+
/** Which level decided what a leaf draws. */
|
|
6
|
+
decided?: LeafClient['decided'];
|
|
7
|
+
children?: StudioNode[];
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Every kind of node a page's tree holds, in the order a walk asks for them. Every walk
|
|
11
|
+
* goes through a map keyed by these, so TypeScript refuses one that leaves a kind out.
|
|
12
|
+
*/
|
|
13
|
+
export declare const nodeKinds: readonly ['layout', 'field', 'form', 'list', 'button', 'title', 'nav', 'controls', 'component', 'row', 'separator', 'tabs', 'accordion', 'section'];
|
|
14
|
+
export type NodeKind = (typeof nodeKinds)[number];
|
|
15
|
+
/** The node of one kind. */
|
|
16
|
+
export type NodeOf<Kind extends NodeKind> = Extract<NodeClient, Record<Kind, unknown>>;
|
|
17
|
+
/** Which kind a node is: the first kind whose key it holds. A bare name is a leaf. */
|
|
18
|
+
export declare const kindOf: (node: unknown) => NodeKind | undefined;
|
|
19
|
+
/** The regions a shell or a layout fills, each with its nodes. */
|
|
20
|
+
export declare const regionsIn: (frame: RegionsClient) => [string, NodeClient[]][];
|
|
21
|
+
/** One screen as the tree `mithaq screens` prints. */
|
|
22
|
+
export declare const studioTree: (name: string, screen: ScreenClient) => StudioNode;
|
|
23
|
+
/** The tree as text, two spaces a level, which is what `mithaq screens` prints. */
|
|
24
|
+
export declare const studioText: (node: StudioNode, depth?: number) => string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { referencesIn } from './client.js';
|
|
2
|
+
const named = (label, reference) => ({ label: `${label} ${reference}` });
|
|
3
|
+
const group = (label, children) => ({ label, ...(children.length && { children }) });
|
|
4
|
+
/**
|
|
5
|
+
* Every kind of node a page's tree holds, in the order a walk asks for them. Every walk
|
|
6
|
+
* goes through a map keyed by these, so TypeScript refuses one that leaves a kind out.
|
|
7
|
+
*/
|
|
8
|
+
export const nodeKinds = ['layout', 'field', 'form', 'list', 'button', 'title', 'nav', 'controls', 'component', 'row', 'separator', 'tabs', 'accordion', 'section'];
|
|
9
|
+
/** Which kind a node is: the first kind whose key it holds. A bare name is a leaf. */
|
|
10
|
+
export const kindOf = (node) => typeof node === 'string' ? 'field' : node && typeof node === 'object' ? nodeKinds.find((kind) => kind in node) : undefined;
|
|
11
|
+
/** The regions a shell or a layout fills, each with its nodes. */
|
|
12
|
+
export const regionsIn = (frame) => Object.entries(frame).filter((entry) => !['shell', 'layout', 'when'].includes(entry[0]));
|
|
13
|
+
const leafOf = (leaf) => {
|
|
14
|
+
const [place, reference] = leaf.component !== undefined ? ['control', leaf.component] : ['cell', leaf.cell ?? ''];
|
|
15
|
+
return { ...named(`${leaf.field}: ${place}`, reference), ...(leaf.decided && { decided: leaf.decided }) };
|
|
16
|
+
};
|
|
17
|
+
const frameOf = (label, reference, frame) => ({
|
|
18
|
+
...named(label, reference),
|
|
19
|
+
children: regionsIn(frame).map(([region, nodes]) => group(`region ${region}`, nodes.map(nodeOf))),
|
|
20
|
+
});
|
|
21
|
+
const drawnBy = (node) => (node.component !== undefined ? [named('component', node.component)] : []);
|
|
22
|
+
const studioOf = {
|
|
23
|
+
layout: (node) => frameOf('layout', node.layout, node),
|
|
24
|
+
field: (node) => leafOf(node),
|
|
25
|
+
form: (node) => group('form', [...drawnBy(node), ...node.form.map(nodeOf)]),
|
|
26
|
+
list: (node) => {
|
|
27
|
+
const { columns, filters, search, empty, menu, sort, limit, of, where } = node.list;
|
|
28
|
+
return group(of ? `list of ${of} where ${where}` : 'list', [
|
|
29
|
+
...drawnBy(node),
|
|
30
|
+
...columns.map(leafOf),
|
|
31
|
+
...(filters ? [{ label: `filters ${filters.join(', ')}` }] : []),
|
|
32
|
+
...(search ? [{ label: `search ${search.join(', ')}` }] : []),
|
|
33
|
+
...(empty !== undefined ? [named('empty', empty)] : []),
|
|
34
|
+
...(menu !== undefined ? [named('menu', menu)] : []),
|
|
35
|
+
...(sort !== undefined ? [{ label: `sort ${sort}` }] : []),
|
|
36
|
+
...(limit !== undefined ? [{ label: `limit ${limit}` }] : []),
|
|
37
|
+
...['toolbar', 'bulk', 'row'].flatMap((region) => (node.list[region] ? [group(`region ${region}`, node.list[region].map(nodeOf))] : [])),
|
|
38
|
+
]);
|
|
39
|
+
},
|
|
40
|
+
button: (node) => group(`button operation ${node.button}${node.confirm ? ' (confirm)' : ''}`, drawnBy(node)),
|
|
41
|
+
title: () => ({ label: 'title' }),
|
|
42
|
+
nav: () => ({ label: 'nav' }),
|
|
43
|
+
controls: () => ({ label: 'controls' }),
|
|
44
|
+
component: (node) => named('component', node.component),
|
|
45
|
+
row: (node) => group(node.resizable ? 'row resizable' : 'row', node.row.map(nodeOf)),
|
|
46
|
+
separator: () => ({ label: 'separator' }),
|
|
47
|
+
tabs: (node) => group('tabs', node.tabs.map((tab) => group(`tab ${tab.tab}`, tab.fields.map(nodeOf)))),
|
|
48
|
+
accordion: (node) => group('accordion', node.accordion.map((item) => group(`item ${item.item}`, item.fields.map(nodeOf)))),
|
|
49
|
+
section: (node) => group(`section ${node.section}`, node.fields.map(nodeOf)),
|
|
50
|
+
};
|
|
51
|
+
const nodeOf = (node) => {
|
|
52
|
+
const line = studioOf[kindOf(node)](node);
|
|
53
|
+
return node.when ? { ...line, label: `${line.label} when ${JSON.stringify(node.when)}` } : line;
|
|
54
|
+
};
|
|
55
|
+
const filesOf = (value) => new Set(referencesIn(value).map((reference) => reference.slice(0, reference.indexOf('#'))));
|
|
56
|
+
/** One screen as the tree `mithaq screens` prints. */
|
|
57
|
+
export const studioTree = (name, screen) => ({
|
|
58
|
+
label: `screen ${name} ${screen.path}`,
|
|
59
|
+
children: [
|
|
60
|
+
group('providers', screen.providers.map((one) => named('provider', one))),
|
|
61
|
+
group('pages', Object.entries(screen.pages).map(([page, one]) => group(`page ${page} ${one.path}`, [named('component', one.component)]))),
|
|
62
|
+
group('nav', screen.nav.map((entry) => ({ label: `${entry.kayan ?? 'screen'} ${entry.page} ${entry.path}${entry.permission ? ` (${entry.permission})` : ''}` }))),
|
|
63
|
+
group('kayan', Object.values(screen.kayan).map((kayan) => group(kayan.name, Object.entries(kayan.pages).map(([page, one]) => group(`page ${page} ${one.path}${'mode' in one && one.mode ? ` (${one.mode})` : ''}`, 'component' in one ? [named('component', one.component)] : [frameOf('shell', one.page.shell, one.page)]))))),
|
|
64
|
+
group('files', [...filesOf(screen)].sort().map((file) => ({ label: file }))),
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
/** The tree as text, two spaces a level, which is what `mithaq screens` prints. */
|
|
68
|
+
export const studioText = (node, depth = 0) => [`${' '.repeat(depth)}${node.label}${node.decided ? ` (${node.decided})` : ''}`, ...(node.children ?? []).map((child) => studioText(child, depth + 1))].join('\n');
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AuditHandler, ContextOf, ProjectStep, Step } from '../asas/runner/steps.ts';
|
|
2
|
+
import type { Authorize } from '../asas/runner/write.ts';
|
|
3
|
+
export { write } from '../asas/runner/write.ts';
|
|
4
|
+
export { bulk } from '../asas/runner/bulk.ts';
|
|
5
|
+
export { readers as read } from '../asas/runner/read.ts';
|
|
6
|
+
/** The runner as a target, `capability: 'service'`: the steps every write runs, its limits, its access evaluator and its defaults. */
|
|
7
|
+
export type ServiceTarget = {
|
|
8
|
+
readonly capability: 'service';
|
|
9
|
+
readonly type?: 'service';
|
|
10
|
+
/** Not a key of the runner: a kayan declares its own operations, spreading `defaultOperations`. */
|
|
11
|
+
readonly operations?: never;
|
|
12
|
+
/** The steps every write runs inside its transaction, after the row is loaded and before the operation's own list. */
|
|
13
|
+
readonly beforeSteps?: readonly ProjectStep[];
|
|
14
|
+
/** The steps every write runs inside its transaction after the list, before the commit: `afterSteps: [audit]`. An operation may write its own. */
|
|
15
|
+
readonly afterSteps?: readonly ProjectStep[];
|
|
16
|
+
/** The steps every write runs after the commit, where the row is durable and visible to every connection. */
|
|
17
|
+
readonly afterCommit?: readonly ProjectStep[];
|
|
18
|
+
/** The keys this project adds to every step's `ctx.context`, as a type: `context: {} as { requestId: string }`. */
|
|
19
|
+
readonly context?: Record<string, unknown>;
|
|
20
|
+
/** The sink explicit audit steps call before commit. */
|
|
21
|
+
readonly audit?: AuditHandler<never>;
|
|
22
|
+
/** The deepest a read or a write may nest another before it is refused. */
|
|
23
|
+
readonly depth: number;
|
|
24
|
+
/** The page a `find` or `findVersions` answers when its caller names no `limit`. */
|
|
25
|
+
readonly defaultLimit: number;
|
|
26
|
+
/** The most rows a served `find` or `findVersions` may ask for; a larger `limit` is a 400. Local calls are not held to it. */
|
|
27
|
+
readonly maxLimit: number;
|
|
28
|
+
/** How many of a child's rows one populated `join` carries where the join field names no `defaultLimit`. */
|
|
29
|
+
readonly joinLimit: number;
|
|
30
|
+
/** The most rows one bulk write may match; a larger match is refused before any row is written. */
|
|
31
|
+
readonly bulkLimit: number;
|
|
32
|
+
/** The access evaluator every operation uses before application steps. */
|
|
33
|
+
readonly authorize: Authorize;
|
|
34
|
+
/**
|
|
35
|
+
* The field that marks a row deleted, for the whole project. A kayan that declares it is
|
|
36
|
+
* soft-deleted: `delete` writes the moment there and every managed read leaves the row out.
|
|
37
|
+
*/
|
|
38
|
+
readonly softDelete?: string;
|
|
39
|
+
/** The default store, the database a write lands in when a kayan names none. */
|
|
40
|
+
readonly store: string;
|
|
41
|
+
/** The default schema target, what a write is validated against when a kayan names none. */
|
|
42
|
+
readonly schema: string;
|
|
43
|
+
};
|
|
44
|
+
/** The template's depth cap: 8. A larger cap only delays the throw and holds the transaction open longer. */
|
|
45
|
+
export declare const defaultDepth = 8;
|
|
46
|
+
/** The template's page size: 10. */
|
|
47
|
+
export declare const defaultLimit = 10;
|
|
48
|
+
/** The template's largest served page: 100. A `limit` outside 1 to the maximum is refused, never clamped. */
|
|
49
|
+
export declare const defaultMaxLimit = 100;
|
|
50
|
+
/** The template's join page: 10. */
|
|
51
|
+
export declare const defaultJoinLimit = 10;
|
|
52
|
+
/** The template's bulk ceiling: 1000. */
|
|
53
|
+
export declare const defaultBulkLimit = 1000;
|
|
54
|
+
type ServiceLists<Extra extends Record<string, unknown>> = {
|
|
55
|
+
readonly beforeSteps?: readonly Step<Record<string, unknown>, Record<string, unknown>, unknown, unknown, unknown, Extra>[];
|
|
56
|
+
readonly afterSteps?: readonly Step<Record<string, unknown>, Record<string, unknown>, unknown, unknown, unknown, Extra>[];
|
|
57
|
+
readonly afterCommit?: readonly Step<Record<string, unknown>, Record<string, unknown>, unknown, unknown, unknown, Extra>[];
|
|
58
|
+
};
|
|
59
|
+
/** Core's runner as the service target of a build. */
|
|
60
|
+
export declare const serviceTarget: <const Service extends Omit<ServiceTarget, 'capability' | 'type'>>(service: Service & NoInfer<ServiceLists<ContextOf<Service>>>) => Service & {
|
|
61
|
+
readonly capability: 'service';
|
|
62
|
+
readonly type: 'service';
|
|
63
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { write } from '../asas/runner/write.js';
|
|
2
|
+
export { bulk } from '../asas/runner/bulk.js';
|
|
3
|
+
export { readers as read } from '../asas/runner/read.js';
|
|
4
|
+
/** The template's depth cap: 8. A larger cap only delays the throw and holds the transaction open longer. */
|
|
5
|
+
export const defaultDepth = 8;
|
|
6
|
+
/** The template's page size: 10. */
|
|
7
|
+
export const defaultLimit = 10;
|
|
8
|
+
/** The template's largest served page: 100. A `limit` outside 1 to the maximum is refused, never clamped. */
|
|
9
|
+
export const defaultMaxLimit = 100;
|
|
10
|
+
/** The template's join page: 10. */
|
|
11
|
+
export const defaultJoinLimit = 10;
|
|
12
|
+
/** The template's bulk ceiling: 1000. */
|
|
13
|
+
export const defaultBulkLimit = 1000;
|
|
14
|
+
/** Core's runner as the service target of a build. */
|
|
15
|
+
export const serviceTarget = (service) => ({ capability: 'service', type: 'service', ...service });
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// No imports, so `mithaq/admin` hands it to the browser without the server's crypto.
|
|
2
|
+
const svg = 'image/svg+xml';
|
|
3
|
+
/**
|
|
4
|
+
* Whether a type is one a kayan takes: `image/*` takes every `image/` type except
|
|
5
|
+
* SVG, a document that runs script, which is taken only where the list names it.
|
|
6
|
+
*/
|
|
7
|
+
export const allowedType = (type, mimeTypes) => type === svg ? mimeTypes.includes(svg) : mimeTypes.some((one) => type.startsWith(one.replace('*', '')));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '../mithaq/standard-schema.ts';
|
|
2
|
+
import type { ResolvedField } from '../mithaq/types.ts';
|
|
3
|
+
import { type StorageConfig } from './storage.ts';
|
|
4
|
+
/** A relationship to a kayan that holds files, drawn as a file picker. */
|
|
5
|
+
export declare const upload: import("../base/kinds.ts").Kind<string | number, "defaultValue" | "index" | "onDelete" | "providedBy" | "required" | "unique"> & {
|
|
6
|
+
targets: {
|
|
7
|
+
readonly drizzle: import("../targets.ts").ColumnBuilder;
|
|
8
|
+
readonly zod: (field: ResolvedField) => import("zod").ZodArray<import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString> | import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString;
|
|
9
|
+
readonly standardSchema: (field: ResolvedField) => StandardSchemaV1<unknown, number | string>;
|
|
10
|
+
readonly screen: {
|
|
11
|
+
readonly control: "upload";
|
|
12
|
+
readonly cell: "upload";
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
} & {
|
|
16
|
+
pointsAtRow: true;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The rukn a project lays down for files: `arkan: [base, storage(storageConfig)]`.
|
|
20
|
+
* The config holds the signing secret, so the module that builds the language stays on the server.
|
|
21
|
+
*/
|
|
22
|
+
export declare const storage: (config: StorageConfig) => {
|
|
23
|
+
readonly name: "storage";
|
|
24
|
+
readonly kinds: {
|
|
25
|
+
readonly upload: import("../base/kinds.ts").Kind<string | number, "defaultValue" | "index" | "onDelete" | "providedBy" | "required" | "unique"> & {
|
|
26
|
+
targets: {
|
|
27
|
+
readonly drizzle: import("../targets.ts").ColumnBuilder;
|
|
28
|
+
readonly zod: (field: ResolvedField) => import("zod").ZodArray<import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString> | import("zod").ZodGUID | import("zod").ZodInt32 | import("zod").ZodString;
|
|
29
|
+
readonly standardSchema: (field: ResolvedField) => StandardSchemaV1<unknown, number | string>;
|
|
30
|
+
readonly screen: {
|
|
31
|
+
readonly control: "upload";
|
|
32
|
+
readonly cell: "upload";
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
} & {
|
|
36
|
+
pointsAtRow: true;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
readonly targets: {
|
|
40
|
+
storage: import("./storage.ts").StorageTarget;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { kind } from '../base/kinds.js';
|
|
2
|
+
import { defineRukn } from '../mithaq/rukn.js';
|
|
3
|
+
import { kinds as drizzleKinds } from '../drizzle/kinds.js';
|
|
4
|
+
import { kinds as zodKinds } from '../zod/kinds.js';
|
|
5
|
+
import { storageTarget } from './storage.js';
|
|
6
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
+
const received = (value) => (value === null ? 'null' : typeof value);
|
|
8
|
+
const notAnId = ({ kind, value }) => {
|
|
9
|
+
if (kind === 'number')
|
|
10
|
+
return typeof value === 'number' && Number.isFinite(value) ? undefined : `Invalid input: expected number, received ${received(value)}`;
|
|
11
|
+
if (typeof value !== 'string')
|
|
12
|
+
return `Invalid input: expected string, received ${received(value)}`;
|
|
13
|
+
return kind === 'uuid' && !uuid.test(value) ? 'Invalid GUID' : undefined;
|
|
14
|
+
};
|
|
15
|
+
/** A relationship to a kayan that holds files, drawn as a file picker. */
|
|
16
|
+
export const upload = kind((value) => typeof value === 'number' || typeof value === 'string')({
|
|
17
|
+
label: { en: 'Upload', ar: 'ملف مرفوع' },
|
|
18
|
+
message: { en: 'Pick one of the listed files.', ar: 'اختر ملفا من القائمة.' },
|
|
19
|
+
rules: ['providedBy', 'required', 'unique', 'index', 'defaultValue', 'onDelete'],
|
|
20
|
+
pointsAtRow: true,
|
|
21
|
+
// A file a row still names is never deleted from under it, whether or not the field is required.
|
|
22
|
+
onDelete: 'restrict',
|
|
23
|
+
targets: {
|
|
24
|
+
drizzle: drizzleKinds.relationship,
|
|
25
|
+
zod: zodKinds.relationship,
|
|
26
|
+
standardSchema: (field) => ({
|
|
27
|
+
'~standard': {
|
|
28
|
+
version: 1,
|
|
29
|
+
vendor: 'mithaq',
|
|
30
|
+
validate: (value) => {
|
|
31
|
+
const message = notAnId({ kind: field.target?.fields.id?.kind, value });
|
|
32
|
+
return message ? { issues: [{ message }] } : { value: value };
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
screen: { control: 'upload', cell: 'upload' },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* The rukn a project lays down for files: `arkan: [base, storage(storageConfig)]`.
|
|
41
|
+
* The config holds the signing secret, so the module that builds the language stays on the server.
|
|
42
|
+
*/
|
|
43
|
+
export const storage = (config) => defineRukn({ name: 'storage', kinds: { upload }, targets: { storage: storageTarget(config) } });
|