@trypattern/sdk 0.0.0-stage → 0.1.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/README.md +127 -2
- package/dist/access.d.ts +31 -0
- package/dist/access.js +72 -0
- package/dist/actions.d.ts +57 -0
- package/dist/actions.js +198 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +34 -0
- package/dist/app.d.ts +106 -0
- package/dist/app.js +224 -0
- package/dist/appearance.d.ts +7 -0
- package/dist/appearance.js +2 -0
- package/dist/auth/sessionApi.d.ts +41 -0
- package/dist/auth/sessionApi.js +158 -0
- package/dist/auth/sessionStore.d.ts +20 -0
- package/dist/auth/sessionStore.js +40 -0
- package/dist/auth/user.d.ts +11 -0
- package/dist/auth/user.js +9 -0
- package/dist/builtins/Account.d.ts +25 -0
- package/dist/builtins/Account.js +15 -0
- package/dist/builtins/Palette.d.ts +29 -0
- package/dist/builtins/Palette.js +63 -0
- package/dist/builtins/SignIn.d.ts +28 -0
- package/dist/builtins/SignIn.js +60 -0
- package/dist/builtins/Toasts.d.ts +32 -0
- package/dist/builtins/Toasts.js +24 -0
- package/dist/builtins/actions.d.ts +2 -0
- package/dist/builtins/actions.js +45 -0
- package/dist/cache.d.ts +24 -0
- package/dist/cache.js +112 -0
- package/dist/cli/app.d.ts +2 -0
- package/dist/cli/app.js +1329 -0
- package/dist/cli/screenshot.d.ts +1 -0
- package/dist/cli/screenshot.js +96 -0
- package/dist/cli/tools.d.ts +12 -0
- package/dist/cli/tools.js +35 -0
- package/dist/components/AppFrame.d.ts +14 -0
- package/dist/components/AppFrame.js +64 -0
- package/dist/components/AppearancePicker.d.ts +1 -0
- package/dist/components/AppearancePicker.js +12 -0
- package/dist/components/Avatar.d.ts +9 -0
- package/dist/components/Avatar.js +63 -0
- package/dist/components/Badge.d.ts +11 -0
- package/dist/components/Badge.js +87 -0
- package/dist/components/Button.d.ts +23 -0
- package/dist/components/Button.js +249 -0
- package/dist/components/Card.d.ts +12 -0
- package/dist/components/Card.js +38 -0
- package/dist/components/CommandMenu.d.ts +23 -0
- package/dist/components/CommandMenu.js +214 -0
- package/dist/components/CommandPalette.d.ts +1 -0
- package/dist/components/CommandPalette.js +7 -0
- package/dist/components/Content.d.ts +7 -0
- package/dist/components/Content.js +24 -0
- package/dist/components/ContextMenu.d.ts +8 -0
- package/dist/components/ContextMenu.js +23 -0
- package/dist/components/Dialog.d.ts +11 -0
- package/dist/components/Dialog.js +81 -0
- package/dist/components/Divider.d.ts +6 -0
- package/dist/components/Divider.js +15 -0
- package/dist/components/EmptyState.d.ts +8 -0
- package/dist/components/EmptyState.js +24 -0
- package/dist/components/Form.d.ts +9 -0
- package/dist/components/Form.js +23 -0
- package/dist/components/Icon.d.ts +10 -0
- package/dist/components/Icon.js +22 -0
- package/dist/components/IconButton.d.ts +14 -0
- package/dist/components/IconButton.js +53 -0
- package/dist/components/Kbd.d.ts +6 -0
- package/dist/components/Kbd.js +67 -0
- package/dist/components/Link.d.ts +9 -0
- package/dist/components/Link.js +35 -0
- package/dist/components/ListRow.d.ts +12 -0
- package/dist/components/ListRow.js +62 -0
- package/dist/components/LoadingRows.d.ts +5 -0
- package/dist/components/LoadingRows.js +6 -0
- package/dist/components/Menu.d.ts +12 -0
- package/dist/components/Menu.js +33 -0
- package/dist/components/MenuItems.d.ts +21 -0
- package/dist/components/MenuItems.js +32 -0
- package/dist/components/Placement.d.ts +21 -0
- package/dist/components/Placement.js +1 -0
- package/dist/components/Popup.d.ts +8 -0
- package/dist/components/Popup.js +43 -0
- package/dist/components/PopupItem.d.ts +8 -0
- package/dist/components/PopupItem.js +51 -0
- package/dist/components/PropertyRow.d.ts +7 -0
- package/dist/components/PropertyRow.js +23 -0
- package/dist/components/Row.d.ts +16 -0
- package/dist/components/Row.js +27 -0
- package/dist/components/Screen.d.ts +13 -0
- package/dist/components/Screen.js +56 -0
- package/dist/components/Scrim.d.ts +8 -0
- package/dist/components/Scrim.js +40 -0
- package/dist/components/ScrollArea.d.ts +15 -0
- package/dist/components/ScrollArea.js +13 -0
- package/dist/components/SegmentedControl.d.ts +17 -0
- package/dist/components/SegmentedControl.js +68 -0
- package/dist/components/Select.d.ts +17 -0
- package/dist/components/Select.js +107 -0
- package/dist/components/SettingsRow.d.ts +8 -0
- package/dist/components/SettingsRow.js +32 -0
- package/dist/components/Sidebar.d.ts +20 -0
- package/dist/components/Sidebar.js +138 -0
- package/dist/components/SignInScreen.d.ts +1 -0
- package/dist/components/SignInScreen.js +40 -0
- package/dist/components/Size.d.ts +1 -0
- package/dist/components/Size.js +1 -0
- package/dist/components/Skeleton.d.ts +8 -0
- package/dist/components/Skeleton.js +48 -0
- package/dist/components/Spinner.d.ts +6 -0
- package/dist/components/Spinner.js +54 -0
- package/dist/components/Stack.d.ts +14 -0
- package/dist/components/Stack.js +19 -0
- package/dist/components/Switch.d.ts +10 -0
- package/dist/components/Switch.js +71 -0
- package/dist/components/Text.d.ts +21 -0
- package/dist/components/Text.js +77 -0
- package/dist/components/TextField.d.ts +25 -0
- package/dist/components/TextField.js +68 -0
- package/dist/components/Toaster.d.ts +1 -0
- package/dist/components/Toaster.js +83 -0
- package/dist/components/Tooltip.d.ts +8 -0
- package/dist/components/Tooltip.js +43 -0
- package/dist/components/controlLook.d.ts +60 -0
- package/dist/components/controlLook.js +67 -0
- package/dist/components/frame.d.ts +4 -0
- package/dist/components/frame.js +2 -0
- package/dist/components/icons.d.ts +58 -0
- package/dist/components/icons.js +105 -0
- package/dist/components/interaction.d.ts +12 -0
- package/dist/components/interaction.js +22 -0
- package/dist/components/spacing.d.ts +118 -0
- package/dist/components/spacing.js +46 -0
- package/dist/components/tones.d.ts +28 -0
- package/dist/components/tones.js +12 -0
- package/dist/entityStore.d.ts +46 -0
- package/dist/entityStore.js +160 -0
- package/dist/flows/accessFlows.d.ts +9 -0
- package/dist/flows/accessFlows.js +151 -0
- package/dist/flows/browser.d.ts +32 -0
- package/dist/flows/browser.js +164 -0
- package/dist/flows/contactSheet.d.ts +1 -0
- package/dist/flows/contactSheet.js +63 -0
- package/dist/flows/flowBrowser.d.ts +23 -0
- package/dist/flows/flowBrowser.js +21 -0
- package/dist/flows/flowSetup.d.ts +3 -0
- package/dist/flows/flowSetup.js +35 -0
- package/dist/flows/flows.d.ts +41 -0
- package/dist/flows/flows.js +148 -0
- package/dist/flows/screenshotName.d.ts +1 -0
- package/dist/flows/screenshotName.js +6 -0
- package/dist/gate.d.ts +8 -0
- package/dist/gate.js +52 -0
- package/dist/hooks/appContext.d.ts +14 -0
- package/dist/hooks/appContext.js +5 -0
- package/dist/hooks/appHooks.d.ts +21 -0
- package/dist/hooks/appHooks.js +24 -0
- package/dist/hooks/useAnimatesOnChange.d.ts +1 -0
- package/dist/hooks/useAnimatesOnChange.js +14 -0
- package/dist/hooks/useStoreState.d.ts +6 -0
- package/dist/hooks/useStoreState.js +5 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.js +19 -0
- package/dist/memoryApi.d.ts +19 -0
- package/dist/memoryApi.js +42 -0
- package/dist/model.d.ts +94 -0
- package/dist/model.js +243 -0
- package/dist/page.d.ts +11 -0
- package/dist/page.js +9 -0
- package/dist/platform.d.ts +1 -0
- package/dist/platform.js +5 -0
- package/dist/react.d.ts +46 -0
- package/dist/react.js +38 -0
- package/dist/registered.d.ts +54 -0
- package/dist/registered.js +17 -0
- package/dist/routes.d.ts +38 -0
- package/dist/routes.js +21 -0
- package/dist/rules/checkAllFiles.d.ts +1 -0
- package/dist/rules/checkAllFiles.js +741 -0
- package/dist/rules/checkApiRouting.d.ts +1 -0
- package/dist/rules/checkApiRouting.js +112 -0
- package/dist/rules/lintPlugin.d.ts +252 -0
- package/dist/rules/lintPlugin.js +1157 -0
- package/dist/rules/messages.d.ts +48 -0
- package/dist/rules/messages.js +48 -0
- package/dist/rules/themeTokens.d.ts +6 -0
- package/dist/rules/themeTokens.js +64 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/server/auth.d.ts +115 -0
- package/dist/server/auth.js +131 -0
- package/dist/server/authService.d.ts +9 -0
- package/dist/server/authService.js +50 -0
- package/dist/server/createServer.d.ts +357 -0
- package/dist/server/createServer.js +62 -0
- package/dist/server/env.d.ts +15 -0
- package/dist/server/env.js +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +5 -0
- package/dist/server/session.d.ts +28 -0
- package/dist/server/session.js +54 -0
- package/dist/server/sessions.d.ts +92 -0
- package/dist/server/sessions.js +47 -0
- package/dist/server/signInWire.d.ts +52 -0
- package/dist/server/signInWire.js +22 -0
- package/dist/server/tables.d.ts +242 -0
- package/dist/server/tables.js +246 -0
- package/dist/server/users.d.ts +189 -0
- package/dist/server/users.js +49 -0
- package/dist/shortcuts.d.ts +3 -0
- package/dist/shortcuts.js +40 -0
- package/dist/start.d.ts +20 -0
- package/dist/start.js +121 -0
- package/dist/state.d.ts +14 -0
- package/dist/state.js +17 -0
- package/dist/store.d.ts +43 -0
- package/dist/store.js +198 -0
- package/dist/subscribers.d.ts +5 -0
- package/dist/subscribers.js +13 -0
- package/dist/table.d.ts +143 -0
- package/dist/table.js +91 -0
- package/dist/tables.d.ts +77 -0
- package/dist/tables.js +216 -0
- package/dist/theme/reset.css +84 -0
- package/dist/theme/scales.d.ts +76 -0
- package/dist/theme/scales.js +82 -0
- package/dist/theme/tokens.stylex.d.ts +218 -0
- package/dist/theme/tokens.stylex.js +233 -0
- package/dist/vite/commandBridge.d.ts +2 -0
- package/dist/vite/commandBridge.js +46 -0
- package/dist/vite/designManifest.d.ts +25 -0
- package/dist/vite/designManifest.js +191 -0
- package/dist/vite/localWorker.d.ts +4 -0
- package/dist/vite/localWorker.js +59 -0
- package/dist/vite/rootAlias.d.ts +2 -0
- package/dist/vite/rootAlias.js +15 -0
- package/dist/vite/vite.d.ts +7 -0
- package/dist/vite/vite.js +88 -0
- package/dist/wire.d.ts +7 -0
- package/dist/wire.js +1 -0
- package/package.json +99 -3
- package/presets/lintPlugin.js +7 -0
- package/presets/oxlint.json +167 -0
- package/presets/register.d.ts +19 -0
- package/presets/server.d.ts +2 -0
- package/presets/tsconfig.app.json +29 -0
- package/presets/tsconfig.server.json +27 -0
package/README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @trypattern/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The engine every Pattern web app runs on. Blueprints and customer apps depend on it; they never
|
|
4
|
+
copy it.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
export const admins: string[] = []
|
|
8
|
+
|
|
9
|
+
export const tables = defineTables({
|
|
10
|
+
issues: {
|
|
11
|
+
columns: { title: z.string().min(1), assignee: z.string().nullable() },
|
|
12
|
+
access: { read: 'signedIn', create: 'signedIn', update: 'signedIn', delete: 'signedIn' },
|
|
13
|
+
writes: 'optimistic',
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export class Preferences extends Store({
|
|
18
|
+
state: { issueSort: z.enum(['oldest', 'title']).default('oldest') },
|
|
19
|
+
persist: ['issueSort'],
|
|
20
|
+
}) {
|
|
21
|
+
public setIssueSort(issueSort: 'oldest' | 'title') {
|
|
22
|
+
this.set({ issueSort })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class IssueList extends Model({ stores: ['issues', 'preferences'] }) {
|
|
27
|
+
public override onPageOpen() {
|
|
28
|
+
return this.stores.issues.load()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public get issues(): readonly Issue[] {
|
|
32
|
+
return sortIssues(this.stores.issues.state.items, this.stores.preferences.state.issueSort)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public setSort = command({ input: { sort: issuesSort } }, ({ sort }) => {
|
|
36
|
+
this.stores.preferences.setIssueSort(sort)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function IssueListScreen() {
|
|
41
|
+
const issueList = useModel('issueList')
|
|
42
|
+
return issueList.issues.map((issue) => <IssueListRow key={issue.id} issue={issue} />)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class App extends defineApp({
|
|
46
|
+
name: 'Issues',
|
|
47
|
+
tables,
|
|
48
|
+
admins,
|
|
49
|
+
stores: { preferences: Preferences },
|
|
50
|
+
models: { issueList: IssueList },
|
|
51
|
+
routes: defineRoutes([{ id: 'issueList', path: '/issues', Component: IssueListScreen }]),
|
|
52
|
+
}) {}
|
|
53
|
+
|
|
54
|
+
export default createServer({ tables, admins })
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- Layers: the service (Pattern's table API) fetches; a **store** holds data and caches it (one per
|
|
58
|
+
table, plus `Store({ state, persist })` classes for shared browser state); a **model**
|
|
59
|
+
(`Model({ state, persist, stores })`) holds a screen's state, works out answers in public getters
|
|
60
|
+
from its state and the stores it lists, and changes things through `command()` fields; a
|
|
61
|
+
**screen** reads one model with `useModel`. Getters are pure and cached: each records which
|
|
62
|
+
store values it read and is worked out again only when one of them changes, so a toast does not
|
|
63
|
+
re-sort a list. A model publishes a frozen snapshot (state, getters and commands), rebuilt only
|
|
64
|
+
while something is subscribed or when it is read, and notifies only when it actually changed;
|
|
65
|
+
that is what `useModel` returns and what flows assert. Models never read each other: a model
|
|
66
|
+
offers what you can do with it in `get actions()`, and the built-in `actions` store gathers
|
|
67
|
+
them for the command menu and shortcuts. `app check` rejects impure getters (`pure-getters`)
|
|
68
|
+
and getters that return anything but plain JSON; at runtime a getter that tries to change state
|
|
69
|
+
or navigate throws, and in dev and flows a getter that answers twice differently throws.
|
|
70
|
+
- `@trypattern/sdk`: `defineApp({ name, ... })` (routing, one store per table, snapshots, the
|
|
71
|
+
built-in `app.navigate` command, the actions store ordered by `actionGroups`, and the built-ins
|
|
72
|
+
every app gets without listing them: the `account`, `palette` and `signIn` models and the
|
|
73
|
+
`session`, `actions`, `toasts` and `page` stores, with the appearance, Log out and ⌘K actions), `defineRoutes` (route ids are the page names), `startApp` (the browser entry:
|
|
74
|
+
creates the router, with the dev tools `app send` and visual flows use), `Model`, `Store`,
|
|
75
|
+
`command`, `appPath`, `defineActions`, `action`, `rankActions` (the command menu's search),
|
|
76
|
+
`EntityStore`, `TableStore` (`create` takes the columns only), `Cache`, `Gate`, and the types
|
|
77
|
+
`AppStores`, `AppSnapshot`, `AppCommand`, `AppPath`, `AppAction`, `AppActionGroup`,
|
|
78
|
+
`Interaction`, `PublishedOf`, `MessagesOf`.
|
|
79
|
+
- `@trypattern/sdk/schema`: `defineTables` and the types `Row`, `PublicRow` and `Seed`. Every row gets an
|
|
80
|
+
`id` and `createdAt` that Pattern sets. Every table declares `access` (`anyone`, `signedIn`,
|
|
81
|
+
`owner`, `admin` or `server` for read, create, update and delete) and `writes` (`optimistic` or
|
|
82
|
+
`confirmed`), and may add `owned: true` (an `ownerId` the server fills), `public` columns (a
|
|
83
|
+
read-only `public<Table>` store) and `unique` column sets (a 409 `conflict`). `src/access.ts` is
|
|
84
|
+
the one rule engine; the Worker and the flow runner's memory API both use it, and
|
|
85
|
+
`accessFlows({ App })` from `@trypattern/sdk/flows` checks every table's rules against the seed.
|
|
86
|
+
Store writes return `'saved' | 'conflict' | 'forbidden' | 'missing' | 'failed'`. Safe to import
|
|
87
|
+
on the server.
|
|
88
|
+
- `@trypattern/sdk/server`: `createServer({ tables, admins })`, the whole Worker: sign-in through
|
|
89
|
+
auth.trypattern.ai (Google, and emailed 6-digit codes; locally the code prints in the dev log),
|
|
90
|
+
dev sign-in, sessions, `/api/me` and every table under `/api/tables`. A deployed app holds one
|
|
91
|
+
`AUTH_SECRET` that Pattern's deploy sets, and no Google secrets. Also `users`, `sessions`,
|
|
92
|
+
`sqlTables`, `seedSql`, the sign-in wire schemas (`verifiedIdentity`, `exchangeRequest`,
|
|
93
|
+
`exchangeReply`, `sendCodeRequest`, `sendCodeReply`, `verifyCodeRequest`, `verifyCodeReply`) and
|
|
94
|
+
the Worker type `D1Database`.
|
|
95
|
+
- `@trypattern/sdk/react`: `AppProvider` and `useModel`, and the primitives every app draws
|
|
96
|
+
with. The frame: `AppFrame`, `Screen`, `SignInScreen`, `CommandPalette`, `Toaster`,
|
|
97
|
+
`CommandMenu`. Layout:
|
|
98
|
+
`Content`, `Stack`, `Row`, `Card`, `Divider`, `SettingsRow`, `PropertyRow`, `ListRow`, `LoadingRows`, `EmptyState`, `Form`.
|
|
99
|
+
Words: `Text` and `Link`. Controls: `Button` (or a link to a page with `to`), `AppearancePicker`, `IconButton`, `TextField`, `Select`, `Switch`,
|
|
100
|
+
`SegmentedControl`, `Menu`, `ContextMenu`, `Dialog`, `Tooltip`, `Kbd`, `ScrollArea`, `Avatar`,
|
|
101
|
+
`Badge`, `Skeleton`, `Spinner`, `Icon`. Types: `IconName`, `MenuEntry`, `SidebarLink`,
|
|
102
|
+
`Toast`, `SelectOption`, `SegmentedOption`, `CommandMenuItem`, `SpaceName` and `Placement`.
|
|
103
|
+
Everything takes closed choices (`variant`, `tone`, `size`, `gap`, `width`) and a `style`
|
|
104
|
+
limited to placement, and every interactive primitive reports to `startApp`'s `onInteraction`.
|
|
105
|
+
App screens have no styles; `app check` holds them to that.
|
|
106
|
+
- `@trypattern/sdk/tokens.stylex`: the design tokens the primitives read. An app re-values
|
|
107
|
+
`colors`, `radii`, `fonts`, `shadows`, `space`, `fontSizes`, `lineHeights`, `controls`,
|
|
108
|
+
`iconSizes`, `borders` and `widths` with `stylex.createTheme` and passes them to
|
|
109
|
+
`startApp(App, { theme, icons, onInteraction })`, which also renders the app; `defineIcons` and `appearance`
|
|
110
|
+
(system, light or dark) come from `@trypattern/sdk`.
|
|
111
|
+
- Every export above is typed for the app with nothing to declare: `presets/register.d.ts`, which
|
|
112
|
+
the app tsconfig preset loads, fills `Register` from `src/App.ts`, `schema.ts`,
|
|
113
|
+
`src/stores/index.ts` and `src/routes.tsx`. `Model`, `AppStores`, `AppSnapshot`, `AppCommand` and `AppPath` come from `@trypattern/sdk`.
|
|
114
|
+
|
|
115
|
+
- `@trypattern/sdk/flows`: `defineFlow({ App })`; the `flow()` it returns fakes the server
|
|
116
|
+
from the app's tables and the seed in `vite.config.ts`, and runs headless in Node or in Chrome with `app test --visual`.
|
|
117
|
+
- `@trypattern/sdk/vite`: `pattern({ tables, admins, seed })`, the whole Vite and Vitest setup, including the
|
|
118
|
+
`@/` alias for the project root. The seed fills the local database and flows; it never ships.
|
|
119
|
+
A build also writes `dist/design.json` (beside, not inside, what is served): the SDK version,
|
|
120
|
+
every token with the app's overrides marked, each primitive with the literal `variant`, `tone`,
|
|
121
|
+
`size` and layout choices it is used with, the app's own drawn pieces and the tokens they read,
|
|
122
|
+
and its icons. Pattern's dashboard renders the design system from it.
|
|
123
|
+
- `@trypattern/sdk/tsconfig/app.json` and `/tsconfig/server.json`: the presets project tsconfigs extend.
|
|
124
|
+
- The `app` command: start, check, fix, reset, migration, build, send, screenshot, test, sign-in,
|
|
125
|
+
setup.
|
|
126
|
+
|
|
127
|
+
Published to npm as `@trypattern/sdk`, compiled to `dist/`; in this repo the blueprint runs the
|
|
128
|
+
source directly. Still to come: payments, workflows and blocks.
|
package/dist/access.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Table } from './table.ts';
|
|
2
|
+
export type Viewer = {
|
|
3
|
+
userId: string;
|
|
4
|
+
isAdmin: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type WriteOutcome = 'saved' | 'conflict' | 'forbidden' | 'missing' | 'failed';
|
|
7
|
+
export type Refusal = 'signedOut' | 'forbidden' | 'missing';
|
|
8
|
+
export type ReadScope = 'all' | {
|
|
9
|
+
ownerId: string;
|
|
10
|
+
} | 'none';
|
|
11
|
+
type Owned = {
|
|
12
|
+
ownerId?: unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare function adminEmails(admins: readonly string[]): ReadonlySet<string>;
|
|
15
|
+
export declare function viewerOf(user: {
|
|
16
|
+
id: string;
|
|
17
|
+
isAdmin: boolean;
|
|
18
|
+
} | null | undefined): Viewer | null;
|
|
19
|
+
export declare function viewerByEmail(user: {
|
|
20
|
+
id: string;
|
|
21
|
+
email: string;
|
|
22
|
+
} | null | undefined, admins: ReadonlySet<string>): Viewer | null;
|
|
23
|
+
export declare function readScope(table: Table, viewer: Viewer | null): ReadScope;
|
|
24
|
+
export declare function listRefusal(table: Table, viewer: Viewer | null): Refusal | undefined;
|
|
25
|
+
export declare function canSee(table: Table, viewer: Viewer | null, row: Owned): boolean;
|
|
26
|
+
export declare function createRefusal(table: Table, viewer: Viewer | null): Refusal | undefined;
|
|
27
|
+
export declare function changeRefusal(table: Table, operation: 'update' | 'delete', viewer: Viewer | null, existing: Owned | undefined): Refusal | undefined;
|
|
28
|
+
export declare function refusalOutcome(refusal: Refusal): 'forbidden' | 'missing';
|
|
29
|
+
export declare function publicRow(table: Table, row: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
30
|
+
export declare function clashesWith(table: Table, rows: readonly Readonly<Record<string, unknown>>[], row: Readonly<Record<string, unknown>>): boolean;
|
|
31
|
+
export {};
|
package/dist/access.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function adminEmails(admins) {
|
|
2
|
+
return new Set(admins.map((email) => email.trim().toLowerCase()));
|
|
3
|
+
}
|
|
4
|
+
export function viewerOf(user) {
|
|
5
|
+
if (user === null || user === undefined)
|
|
6
|
+
return null;
|
|
7
|
+
return { userId: user.id, isAdmin: user.isAdmin };
|
|
8
|
+
}
|
|
9
|
+
export function viewerByEmail(user, admins) {
|
|
10
|
+
if (user === null || user === undefined)
|
|
11
|
+
return null;
|
|
12
|
+
return { userId: user.id, isAdmin: admins.has(user.email.toLowerCase()) };
|
|
13
|
+
}
|
|
14
|
+
function mayUse(level, viewer) {
|
|
15
|
+
if (level === 'server')
|
|
16
|
+
return false;
|
|
17
|
+
if (level === 'anyone' || viewer?.isAdmin === true)
|
|
18
|
+
return true;
|
|
19
|
+
if (viewer === null)
|
|
20
|
+
return false;
|
|
21
|
+
return level === 'signedIn' || level === 'owner';
|
|
22
|
+
}
|
|
23
|
+
function allows(level, viewer, row) {
|
|
24
|
+
if (!mayUse(level, viewer))
|
|
25
|
+
return false;
|
|
26
|
+
if (level !== 'owner' || viewer?.isAdmin === true)
|
|
27
|
+
return true;
|
|
28
|
+
return viewer !== null && row.ownerId === viewer.userId;
|
|
29
|
+
}
|
|
30
|
+
function refusalFor(viewer) {
|
|
31
|
+
return viewer === null ? 'signedOut' : 'forbidden';
|
|
32
|
+
}
|
|
33
|
+
export function readScope(table, viewer) {
|
|
34
|
+
const level = table.access.read;
|
|
35
|
+
if (!mayUse(level, viewer))
|
|
36
|
+
return 'none';
|
|
37
|
+
if (level === 'owner' && viewer !== null && !viewer.isAdmin) {
|
|
38
|
+
return { ownerId: viewer.userId };
|
|
39
|
+
}
|
|
40
|
+
return 'all';
|
|
41
|
+
}
|
|
42
|
+
export function listRefusal(table, viewer) {
|
|
43
|
+
return readScope(table, viewer) === 'none' ? refusalFor(viewer) : undefined;
|
|
44
|
+
}
|
|
45
|
+
export function canSee(table, viewer, row) {
|
|
46
|
+
const scope = readScope(table, viewer);
|
|
47
|
+
if (scope === 'none')
|
|
48
|
+
return false;
|
|
49
|
+
return scope === 'all' || row.ownerId === scope.ownerId;
|
|
50
|
+
}
|
|
51
|
+
export function createRefusal(table, viewer) {
|
|
52
|
+
return mayUse(table.access.create, viewer) ? undefined : refusalFor(viewer);
|
|
53
|
+
}
|
|
54
|
+
export function changeRefusal(table, operation, viewer, existing) {
|
|
55
|
+
const level = table.access[operation];
|
|
56
|
+
if (!mayUse(level, viewer))
|
|
57
|
+
return refusalFor(viewer);
|
|
58
|
+
if (existing === undefined || !canSee(table, viewer, existing)) {
|
|
59
|
+
return 'missing';
|
|
60
|
+
}
|
|
61
|
+
return allows(level, viewer, existing) ? undefined : 'forbidden';
|
|
62
|
+
}
|
|
63
|
+
export function refusalOutcome(refusal) {
|
|
64
|
+
return refusal === 'missing' ? 'missing' : 'forbidden';
|
|
65
|
+
}
|
|
66
|
+
export function publicRow(table, row) {
|
|
67
|
+
return Object.fromEntries(['id', ...table.public].map((key) => [key, row[key] ?? null]));
|
|
68
|
+
}
|
|
69
|
+
export function clashesWith(table, rows, row) {
|
|
70
|
+
return table.unique.some((columns) => rows.some((other) => other['id'] !== row['id'] &&
|
|
71
|
+
columns.every((column) => other[column] === row[column])));
|
|
72
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Message } from './model.ts';
|
|
2
|
+
import { StoreBase } from './store.ts';
|
|
3
|
+
import type { PageStore } from './page.ts';
|
|
4
|
+
export type Action<Command extends Message = Message> = {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly group: string;
|
|
8
|
+
readonly icon: string;
|
|
9
|
+
readonly keys: readonly string[];
|
|
10
|
+
readonly keywords: string;
|
|
11
|
+
readonly isDanger: boolean;
|
|
12
|
+
readonly isSearchOnly: boolean;
|
|
13
|
+
readonly isHidden: boolean;
|
|
14
|
+
readonly send: readonly Command[];
|
|
15
|
+
};
|
|
16
|
+
export type ActionsBuilder<Context, Item extends Action> = {
|
|
17
|
+
readonly page: string | undefined;
|
|
18
|
+
readonly build: (context: Context) => readonly (Item | false)[];
|
|
19
|
+
};
|
|
20
|
+
type PageOf<Context> = Context extends {
|
|
21
|
+
readonly page: infer Page;
|
|
22
|
+
} ? Exclude<Page, null> & string : never;
|
|
23
|
+
type Entries<Seen, Item extends Action, Command extends Message> = (context: Seen) => readonly ((Item & {
|
|
24
|
+
readonly send: readonly Command[];
|
|
25
|
+
}) | false)[];
|
|
26
|
+
export type DefineActions<Context extends {
|
|
27
|
+
readonly page: unknown;
|
|
28
|
+
}, Item extends Action, Command extends Message> = {
|
|
29
|
+
(build: Entries<Context, Item, Command>): ActionsBuilder<Context, Item>;
|
|
30
|
+
<const Page extends PageOf<Context>>(options: {
|
|
31
|
+
readonly page: Page;
|
|
32
|
+
}, build: Entries<Omit<Context, 'page'> & {
|
|
33
|
+
readonly page: Page;
|
|
34
|
+
}, Item, Command>): ActionsBuilder<Context, Item>;
|
|
35
|
+
};
|
|
36
|
+
export declare function bindActions<Context extends {
|
|
37
|
+
readonly page: unknown;
|
|
38
|
+
}, Item extends Action, Command extends Message>(): DefineActions<Context, Item, Command>;
|
|
39
|
+
type AnyBuilder = ActionsBuilder<never, Action>;
|
|
40
|
+
type ReadableModel = {
|
|
41
|
+
readonly state: object;
|
|
42
|
+
readonly currentState: object;
|
|
43
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
44
|
+
};
|
|
45
|
+
type ActionsState<Item extends Action> = {
|
|
46
|
+
items: readonly Item[];
|
|
47
|
+
};
|
|
48
|
+
export declare class ActionsStore<Item extends Action = Action> extends StoreBase<ActionsState<Item>> {
|
|
49
|
+
#private;
|
|
50
|
+
constructor(page: PageStore, builders: readonly AnyBuilder[], groups: readonly string[], send: (message: Message) => Promise<void>);
|
|
51
|
+
protected initialState(): ActionsState<Item>;
|
|
52
|
+
watch(models: Readonly<Record<string, ReadableModel>>): void;
|
|
53
|
+
pressKey(key: string): Promise<void> | undefined;
|
|
54
|
+
run(action: Item): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
export declare function rankActions<Item extends Action>(actions: readonly Item[], query: string): Item[];
|
|
57
|
+
export {};
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { answerNamesOf } from "./model.js";
|
|
2
|
+
import { StoreBase, isDevelopment, keepEqualParts, plainJsonProblem, readsAreCurrent, tracked, whileBuilding } from "./store.js";
|
|
3
|
+
export function bindActions() {
|
|
4
|
+
function defineActions(first, second) {
|
|
5
|
+
if (typeof first === 'function')
|
|
6
|
+
return { page: undefined, build: first };
|
|
7
|
+
if (second === undefined)
|
|
8
|
+
throw new Error('defineActions() needs entries');
|
|
9
|
+
return { page: first.page, build: second };
|
|
10
|
+
}
|
|
11
|
+
return defineActions;
|
|
12
|
+
}
|
|
13
|
+
const NAMED_READS_ONLY = 'An actions file reads a model by naming a state field or answer, as models.issueDetail.hasNext. It cannot spread a model, list its keys or test `in`, because Pattern reruns the file only when something it named changes.';
|
|
14
|
+
function readOnlyView(model) {
|
|
15
|
+
const answers = answerNamesOf(model);
|
|
16
|
+
const refuse = () => {
|
|
17
|
+
throw new Error(NAMED_READS_ONLY);
|
|
18
|
+
};
|
|
19
|
+
return new Proxy({}, {
|
|
20
|
+
get(_, key) {
|
|
21
|
+
if (typeof key !== 'string')
|
|
22
|
+
return undefined;
|
|
23
|
+
if (key in model.currentState)
|
|
24
|
+
return Reflect.get(model.state, key);
|
|
25
|
+
return answers.includes(key) ? Reflect.get(model, key) : undefined;
|
|
26
|
+
},
|
|
27
|
+
has: refuse,
|
|
28
|
+
ownKeys: refuse,
|
|
29
|
+
getOwnPropertyDescriptor: refuse
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const MOST_PASSES = 4;
|
|
33
|
+
const SEQUENCE_MS = 1000;
|
|
34
|
+
function keysAre(keys, pressed) {
|
|
35
|
+
return (keys.length === pressed.length &&
|
|
36
|
+
pressed.every((key, index) => keys[index] === key));
|
|
37
|
+
}
|
|
38
|
+
function keysStartWith(keys, pressed) {
|
|
39
|
+
return (keys.length > pressed.length &&
|
|
40
|
+
pressed.every((key, index) => keys[index] === key));
|
|
41
|
+
}
|
|
42
|
+
export class ActionsStore extends StoreBase {
|
|
43
|
+
#page;
|
|
44
|
+
#builders;
|
|
45
|
+
#send;
|
|
46
|
+
#memos = new Map();
|
|
47
|
+
#context = {};
|
|
48
|
+
#isRebuilding = false;
|
|
49
|
+
#needsAnotherPass = false;
|
|
50
|
+
#pressed = [];
|
|
51
|
+
#pressedAt = 0;
|
|
52
|
+
#groups;
|
|
53
|
+
constructor(page, builders, groups, send) {
|
|
54
|
+
super();
|
|
55
|
+
this.#page = page;
|
|
56
|
+
this.#builders = builders;
|
|
57
|
+
this.#groups = groups;
|
|
58
|
+
this.#send = send;
|
|
59
|
+
}
|
|
60
|
+
initialState() {
|
|
61
|
+
return { items: [] };
|
|
62
|
+
}
|
|
63
|
+
watch(models) {
|
|
64
|
+
const views = Object.fromEntries(Object.entries(models).map(([name, model]) => [name, readOnlyView(model)]));
|
|
65
|
+
const page = this.#page;
|
|
66
|
+
this.#context = {
|
|
67
|
+
get page() {
|
|
68
|
+
return page.state.page;
|
|
69
|
+
},
|
|
70
|
+
models: views
|
|
71
|
+
};
|
|
72
|
+
for (const model of Object.values(models)) {
|
|
73
|
+
model.subscribe(() => this.#rebuild());
|
|
74
|
+
}
|
|
75
|
+
page.subscribe(() => this.#rebuild());
|
|
76
|
+
this.#rebuild();
|
|
77
|
+
}
|
|
78
|
+
pressKey(key) {
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const earlier = now - this.#pressedAt < SEQUENCE_MS ? this.#pressed : [];
|
|
81
|
+
const pressed = [...earlier, key.toLowerCase()];
|
|
82
|
+
this.#pressedAt = now;
|
|
83
|
+
const items = this.currentState.items;
|
|
84
|
+
const match = items.find((item) => keysAre(item.keys, pressed));
|
|
85
|
+
if (match !== undefined) {
|
|
86
|
+
this.#pressed = [];
|
|
87
|
+
return this.run(match);
|
|
88
|
+
}
|
|
89
|
+
if (items.some((item) => keysStartWith(item.keys, pressed))) {
|
|
90
|
+
this.#pressed = pressed;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
this.#pressed = [];
|
|
94
|
+
return earlier.length > 0 ? this.pressKey(key) : undefined;
|
|
95
|
+
}
|
|
96
|
+
async run(action) {
|
|
97
|
+
for (const message of action.send)
|
|
98
|
+
await this.#send(message);
|
|
99
|
+
}
|
|
100
|
+
#rebuild() {
|
|
101
|
+
if (this.#isRebuilding) {
|
|
102
|
+
this.#needsAnotherPass = true;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.#isRebuilding = true;
|
|
106
|
+
try {
|
|
107
|
+
for (let pass = 1;; pass += 1) {
|
|
108
|
+
this.#needsAnotherPass = false;
|
|
109
|
+
this.#rebuildOnce();
|
|
110
|
+
if (!this.#needsAnotherPass)
|
|
111
|
+
return;
|
|
112
|
+
if (pass === MOST_PASSES) {
|
|
113
|
+
throw new Error('The actions never settle: an actions file reads something that changes whenever the actions change, such as models.palette.results. Actions files read the page and the models a person acts on, never the command menu.');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
this.#isRebuilding = false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
#rebuildOnce() {
|
|
122
|
+
const built = this.#builders
|
|
123
|
+
.flatMap((builder) => this.#evaluate(builder))
|
|
124
|
+
.toSorted((a, b) => this.#groupRank(a) - this.#groupRank(b));
|
|
125
|
+
const items = keepEqualParts(this.currentState.items, built);
|
|
126
|
+
if (items !== this.currentState.items)
|
|
127
|
+
this.set({ items });
|
|
128
|
+
}
|
|
129
|
+
#groupRank(action) {
|
|
130
|
+
const rank = this.#groups.indexOf(action.group);
|
|
131
|
+
return rank === -1 ? this.#groups.length : rank;
|
|
132
|
+
}
|
|
133
|
+
#evaluate(builder) {
|
|
134
|
+
const memo = this.#memos.get(builder);
|
|
135
|
+
if (memo !== undefined && readsAreCurrent(memo.reads))
|
|
136
|
+
return memo.value;
|
|
137
|
+
const { value: built, reads } = whileBuilding(() => tracked(() => this.#build(builder)));
|
|
138
|
+
if (isDevelopment())
|
|
139
|
+
this.#checkPure(builder, built);
|
|
140
|
+
const value = memo === undefined ? built : keepEqualParts(memo.value, built);
|
|
141
|
+
this.#memos.set(builder, { value, reads });
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
#build(builder) {
|
|
145
|
+
const isElsewhere = builder.page !== undefined && this.#page.state.page !== builder.page;
|
|
146
|
+
if (isElsewhere)
|
|
147
|
+
return [];
|
|
148
|
+
return builder
|
|
149
|
+
.build(this.#context)
|
|
150
|
+
.filter((entry) => entry !== false);
|
|
151
|
+
}
|
|
152
|
+
#checkPure(builder, built) {
|
|
153
|
+
const problem = plainJsonProblem(built, 'actions');
|
|
154
|
+
if (problem !== undefined) {
|
|
155
|
+
throw new Error(`Actions must be plain JSON, because flows compare them: ${problem}`);
|
|
156
|
+
}
|
|
157
|
+
const again = whileBuilding(() => this.#build(builder));
|
|
158
|
+
if (JSON.stringify(built) !== JSON.stringify(again)) {
|
|
159
|
+
throw new Error('An actions file gave two different lists for the same page and models, so it is not pure. Actions files must not read the clock, randomness or anything outside page and models.');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function scoreOf(candidate, terms) {
|
|
164
|
+
const label = candidate.label.toLowerCase();
|
|
165
|
+
const haystack = `${label} ${candidate.keywords} ${candidate.group}`.toLowerCase();
|
|
166
|
+
if (!terms.every((term) => haystack.includes(term)))
|
|
167
|
+
return 0;
|
|
168
|
+
const phrase = terms.join(' ');
|
|
169
|
+
if (label.startsWith(phrase))
|
|
170
|
+
return 4;
|
|
171
|
+
if (` ${label}`.includes(` ${phrase}`))
|
|
172
|
+
return 3;
|
|
173
|
+
if (label.includes(phrase))
|
|
174
|
+
return 2;
|
|
175
|
+
return 1;
|
|
176
|
+
}
|
|
177
|
+
export function rankActions(actions, query) {
|
|
178
|
+
const offered = actions.filter((candidate) => !candidate.isHidden);
|
|
179
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
180
|
+
if (terms.length === 0) {
|
|
181
|
+
return offered.filter((candidate) => !candidate.isSearchOnly);
|
|
182
|
+
}
|
|
183
|
+
const order = [...new Set(offered.map((candidate) => candidate.group))];
|
|
184
|
+
const scored = offered.flatMap((candidate) => {
|
|
185
|
+
const score = scoreOf(candidate, terms);
|
|
186
|
+
return score === 0 ? [] : [{ candidate, score }];
|
|
187
|
+
});
|
|
188
|
+
const best = new Map();
|
|
189
|
+
for (const { candidate, score } of scored) {
|
|
190
|
+
best.set(candidate.group, Math.max(best.get(candidate.group) ?? 0, score));
|
|
191
|
+
}
|
|
192
|
+
const rank = (group) => best.get(group) ?? 0;
|
|
193
|
+
return scored
|
|
194
|
+
.toSorted((a, b) => rank(b.candidate.group) - rank(a.candidate.group) ||
|
|
195
|
+
order.indexOf(a.candidate.group) - order.indexOf(b.candidate.group) ||
|
|
196
|
+
b.score - a.score)
|
|
197
|
+
.map(({ candidate }) => candidate);
|
|
198
|
+
}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SessionApi, SessionCall } from './auth/sessionApi.ts';
|
|
2
|
+
import type { Gate } from './gate.ts';
|
|
3
|
+
import type { Seed, Tables } from './table.ts';
|
|
4
|
+
import type { TableCall, TablesApi } from './tables.ts';
|
|
5
|
+
export type AppApi<Group extends Tables> = {
|
|
6
|
+
tables: TablesApi<Group>;
|
|
7
|
+
session: SessionApi;
|
|
8
|
+
};
|
|
9
|
+
export type ApiCall<Group extends Tables> = TableCall<Group> | `session.${SessionCall}`;
|
|
10
|
+
export declare function apiCalls<Group extends Tables>(tables: Group): readonly ApiCall<Group>[];
|
|
11
|
+
export declare function httpApi<Group extends Tables>(tables: Group, gate: Gate): AppApi<Group>;
|
|
12
|
+
export declare function memoryApi<Group extends Tables>(tables: Group, seed: Seed<Group>, { gate, signedInAs, admins }: {
|
|
13
|
+
gate: Gate;
|
|
14
|
+
signedInAs: string | null;
|
|
15
|
+
admins: readonly string[];
|
|
16
|
+
}): AppApi<Group>;
|
|
17
|
+
export declare function browserApi<Group extends Tables>(tables: Group, seed: Seed<Group>, gate: Gate, { inMemory, admins }: {
|
|
18
|
+
inMemory: boolean;
|
|
19
|
+
admins: readonly string[];
|
|
20
|
+
}): AppApi<Group>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { adminEmails, viewerByEmail, viewerOf } from "./access.js";
|
|
2
|
+
import { HttpSessionApi, MemorySessionApi, sessionCalls } from "./auth/sessionApi.js";
|
|
3
|
+
import { httpTables, memoryTables, tableCalls } from "./tables.js";
|
|
4
|
+
export function apiCalls(tables) {
|
|
5
|
+
return [
|
|
6
|
+
...tableCalls(tables),
|
|
7
|
+
...sessionCalls.map((call) => `session.${call}`)
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
export function httpApi(tables, gate) {
|
|
11
|
+
const session = new HttpSessionApi(gate);
|
|
12
|
+
return {
|
|
13
|
+
tables: httpTables(tables, gate, () => session.reportSignedOut()),
|
|
14
|
+
session
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function memoryApi(tables, seed, { gate, signedInAs, admins }) {
|
|
18
|
+
const options = { latencyMs: 0, gate };
|
|
19
|
+
const session = new MemorySessionApi(signedInAs, admins, options);
|
|
20
|
+
return {
|
|
21
|
+
tables: memoryTables(tables, seed, () => viewerOf(session.rememberedUser()), options),
|
|
22
|
+
session
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function browserApi(tables, seed, gate, { inMemory, admins }) {
|
|
26
|
+
const session = new HttpSessionApi(gate);
|
|
27
|
+
const emails = adminEmails(admins);
|
|
28
|
+
return {
|
|
29
|
+
tables: inMemory
|
|
30
|
+
? memoryTables(tables, seed, () => viewerByEmail(session.rememberedUser(), emails), { latencyMs: 0, gate })
|
|
31
|
+
: httpTables(tables, gate, () => session.reportSignedOut()),
|
|
32
|
+
session
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { DataRouter } from 'react-router';
|
|
2
|
+
import type { AppApi } from './api.ts';
|
|
3
|
+
import { SessionStore } from './auth/sessionStore.ts';
|
|
4
|
+
import type { Cache } from './cache.ts';
|
|
5
|
+
import type { Gate } from './gate.ts';
|
|
6
|
+
import { ActionsStore } from './actions.ts';
|
|
7
|
+
import type { Action, ActionsBuilder } from './actions.ts';
|
|
8
|
+
import { Toasts } from './builtins/Toasts.ts';
|
|
9
|
+
import { PageStore } from './page.ts';
|
|
10
|
+
import type { BuiltInModels } from './registered.ts';
|
|
11
|
+
import type { Snapshots } from './store.ts';
|
|
12
|
+
import type { Json, Persisted } from './state.ts';
|
|
13
|
+
import type { DerivedOf, Instances, MessagesOf, ModelClassLike, ModelSnapshots } from './model.ts';
|
|
14
|
+
import type { PageOf, PathOf, RouteDefinition } from './routes.ts';
|
|
15
|
+
import type { Tables } from './table.ts';
|
|
16
|
+
import { PublicTableStore, TableStore } from './tables.ts';
|
|
17
|
+
type Observed = {
|
|
18
|
+
readonly state: object;
|
|
19
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
20
|
+
};
|
|
21
|
+
export type { DataRouter };
|
|
22
|
+
export type ModelServicesOf<Path extends string> = {
|
|
23
|
+
navigate: (to: Path) => Promise<void>;
|
|
24
|
+
};
|
|
25
|
+
type StaticTypeError<Text extends string> = Text;
|
|
26
|
+
type NonJsonGetters<Instance> = {
|
|
27
|
+
[Key in keyof DerivedOf<Instance>]: DerivedOf<Instance>[Key] extends Json | undefined ? never : Key;
|
|
28
|
+
}[keyof DerivedOf<Instance>];
|
|
29
|
+
export type PlainJsonModel<Class> = Class extends abstract new (...args: never[]) => infer Instance ? [NonJsonGetters<Instance>] extends [never] ? Class : StaticTypeError<`Getter ${NonJsonGetters<Instance> & string} must return plain JSON (strings, numbers, booleans, null, and arrays and objects of those, or undefined), because flows compare the snapshot`> : Class;
|
|
30
|
+
export type NavigateCommand<Path extends string> = {
|
|
31
|
+
type: 'app.navigate';
|
|
32
|
+
to: Path;
|
|
33
|
+
};
|
|
34
|
+
export type PressKeyCommand = {
|
|
35
|
+
type: 'app.pressKey';
|
|
36
|
+
key: string;
|
|
37
|
+
};
|
|
38
|
+
export type AppDependencies<Api> = {
|
|
39
|
+
api: Api;
|
|
40
|
+
cache: Cache;
|
|
41
|
+
gate: Gate;
|
|
42
|
+
};
|
|
43
|
+
export type RunningApp<Command, Snapshot extends {
|
|
44
|
+
readonly path: string;
|
|
45
|
+
}> = {
|
|
46
|
+
readonly router: DataRouter;
|
|
47
|
+
send(command: Command): Promise<void>;
|
|
48
|
+
parse(raw: unknown): Command | {
|
|
49
|
+
error: string;
|
|
50
|
+
};
|
|
51
|
+
getSnapshot(): Snapshot;
|
|
52
|
+
whenIdle(): Promise<void>;
|
|
53
|
+
};
|
|
54
|
+
export type StoreClasses = Record<string, new (persisted: Persisted) => Observed>;
|
|
55
|
+
export type StoreInstances<Classes extends StoreClasses> = {
|
|
56
|
+
[Key in keyof Classes]: InstanceType<Classes[Key]>;
|
|
57
|
+
};
|
|
58
|
+
export type TableStores<Group extends Tables> = {
|
|
59
|
+
[Key in keyof Group as Group[Key]['access']['read'] extends 'server' ? never : Key]: TableStore<Group[Key]>;
|
|
60
|
+
};
|
|
61
|
+
export type PublicStores<Group extends Tables> = {
|
|
62
|
+
[Key in keyof Group & string as [Group[Key]['public'][number]] extends [
|
|
63
|
+
never
|
|
64
|
+
] ? never : `public${Capitalize<Key>}`]: PublicTableStore<Group[Key]>;
|
|
65
|
+
};
|
|
66
|
+
export type StoresOf<Group extends Tables, Custom, Item extends Action = Action> = {
|
|
67
|
+
session: SessionStore;
|
|
68
|
+
actions: ActionsStore<Item>;
|
|
69
|
+
toasts: Toasts;
|
|
70
|
+
page: PageStore;
|
|
71
|
+
} & TableStores<Group> & PublicStores<Group> & Custom;
|
|
72
|
+
export type AppDefinition<Group extends Tables, Custom extends StoreClasses, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[]> = {
|
|
73
|
+
name: string;
|
|
74
|
+
tables: Group;
|
|
75
|
+
stores?: Custom;
|
|
76
|
+
models: {
|
|
77
|
+
[Key in keyof Classes]: Key extends keyof BuiltInModels | 'app' ? StaticTypeError<`${Key & string} is a built-in model of @trypattern/sdk: its state, answers and commands come from Pattern. Name yours for what it holds.`> : PlainJsonModel<Classes[Key]>;
|
|
78
|
+
};
|
|
79
|
+
routes: Routes;
|
|
80
|
+
actions?: readonly ActionsBuilder<never, Action>[];
|
|
81
|
+
actionGroups?: readonly string[];
|
|
82
|
+
};
|
|
83
|
+
export type AppClass<Group extends Tables, Instance> = {
|
|
84
|
+
new (router: DataRouter, deps: AppDependencies<AppApi<Group>>): Instance;
|
|
85
|
+
readonly definition: {
|
|
86
|
+
readonly tables: Group;
|
|
87
|
+
readonly routes: readonly RouteDefinition[];
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export type AppSnapshotShape<Models, Stores, Page> = {
|
|
91
|
+
page: Page | null;
|
|
92
|
+
path: string;
|
|
93
|
+
models: ModelSnapshots<Models>;
|
|
94
|
+
stores: Snapshots<Stores>;
|
|
95
|
+
};
|
|
96
|
+
type AppCommandOf<Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[]> = MessagesOf<Instances<BuiltInModels>> | MessagesOf<Instances<Classes>> | NavigateCommand<PathOf<Routes>> | PressKeyCommand;
|
|
97
|
+
export type DefinedApp<Group extends Tables, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Custom extends StoreClasses> = {
|
|
98
|
+
new (router: DataRouter, dependencies: AppDependencies<AppApi<Group>>): RunningApp<AppCommandOf<Classes, Routes>, AppSnapshotShape<Instances<BuiltInModels & Classes>, StoresOf<Group, StoreInstances<Custom>>, PageOf<Routes>>> & {
|
|
99
|
+
readonly name: string;
|
|
100
|
+
readonly models: Instances<BuiltInModels & Classes>;
|
|
101
|
+
readonly stores: StoresOf<Group, StoreInstances<Custom>>;
|
|
102
|
+
pressKey(key: string): Promise<void> | undefined;
|
|
103
|
+
};
|
|
104
|
+
readonly definition: AppDefinition<Group, Custom, Classes, Routes>;
|
|
105
|
+
};
|
|
106
|
+
export declare function defineApp<Group extends Tables, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Custom extends StoreClasses = Record<never, never>>(definition: AppDefinition<Group, Custom, Classes, Routes>): DefinedApp<Group, Classes, Routes, Custom>;
|