@trypattern/sdk 0.1.12 → 0.1.13
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 +73 -12
- package/dist/access.d.ts +0 -4
- package/dist/access.js +0 -5
- package/dist/api.d.ts +3 -1
- package/dist/api.js +5 -23
- package/dist/app.d.ts +20 -5
- package/dist/app.js +32 -2
- package/dist/blocks.d.ts +18 -0
- package/dist/blocks.js +36 -0
- package/dist/bookings.d.ts +400 -0
- package/dist/bookings.js +300 -0
- package/dist/bookingsMemory.d.ts +9 -0
- package/dist/bookingsMemory.js +79 -0
- package/dist/bookingsStore.d.ts +698 -0
- package/dist/bookingsStore.js +129 -0
- package/dist/cli/app.js +143 -59
- package/dist/cli/publishing.d.ts +3 -1
- package/dist/cli/publishing.js +33 -5
- package/dist/cli/starter.d.ts +17 -0
- package/dist/cli/starter.js +140 -0
- package/dist/components/Calendar.d.ts +20 -0
- package/dist/components/Calendar.js +195 -0
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +7 -29
- package/dist/components/Choice.js +27 -13
- package/dist/components/ChoiceGroup.d.ts +3 -1
- package/dist/components/ChoiceGroup.js +3 -4
- package/dist/components/Dialog.d.ts +1 -1
- package/dist/components/Dialog.js +3 -1
- package/dist/components/ListRow.js +2 -1
- package/dist/components/RadioGroup.d.ts +2 -1
- package/dist/components/RadioGroup.js +10 -2
- package/dist/components/Text.d.ts +1 -1
- package/dist/components/Text.js +43 -2
- package/dist/components/calendarDates.d.ts +14 -0
- package/dist/components/calendarDates.js +84 -0
- package/dist/components/controlLook.js +11 -11
- package/dist/components/icons.d.ts +2 -0
- package/dist/components/icons.js +2 -0
- package/dist/components/interaction.d.ts +1 -1
- package/dist/flows/browser.d.ts +7 -1
- package/dist/flows/browser.js +25 -7
- package/dist/flows/flowBrowser.d.ts +2 -2
- package/dist/flows/flowBrowser.js +2 -2
- package/dist/flows/flows.js +8 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/media.d.ts +11 -1
- package/dist/patternAccess.d.ts +11 -2
- package/dist/patternAccess.js +8 -4
- package/dist/publishWire.d.ts +24 -2
- package/dist/publishWire.js +22 -2
- package/dist/react.d.ts +2 -0
- package/dist/react.js +1 -0
- package/dist/rules/checkAllFiles.js +16 -2
- package/dist/rules/lintPlugin.js +1 -0
- package/dist/schema.d.ts +3 -1
- package/dist/schema.js +1 -0
- package/dist/server/bookings.d.ts +12 -0
- package/dist/server/bookings.js +142 -0
- package/dist/server/createServer.d.ts +3 -2
- package/dist/server/createServer.js +13 -4
- package/dist/server/env.d.ts +8 -0
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +1 -0
- package/dist/server/media.d.ts +16 -3
- package/dist/server/media.js +72 -23
- package/dist/server/patternViewer.js +16 -10
- package/dist/server/tables.d.ts +2 -0
- package/dist/server/tables.js +17 -1
- package/dist/start.js +12 -0
- package/dist/table.js +9 -3
- package/dist/theme/scales.d.ts +1 -1
- package/dist/theme/scales.js +1 -0
- package/dist/theme/tokens.stylex.d.ts +18 -0
- package/dist/theme/tokens.stylex.js +19 -1
- package/dist/vite/localWorker.d.ts +2 -0
- package/dist/vite/localWorker.js +48 -3
- package/dist/vite/vite.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,10 +73,11 @@ export default createServer({ tables, admins })
|
|
|
73
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
74
|
creates the router, with the dev tools `app send` and visual flows use), `Model`, `Store`,
|
|
75
75
|
`command`, `appPath`, `defineActions`, `action`, `rankActions` (the command menu's search),
|
|
76
|
-
`EntityStore`, `TableStore` (`create` takes the columns only), `Cache`, `Gate`,
|
|
76
|
+
`EntityStore`, `TableStore` (`create` takes the columns only), `Cache`, `Gate`, the `bookings`
|
|
77
|
+
block (below), and the types
|
|
77
78
|
`AppStores`, `AppSnapshot`, `AppCommand`, `AppPath`, `AppAction`, `AppActionGroup`,
|
|
78
79
|
`Interaction`, `PublishedOf`, `MessagesOf`.
|
|
79
|
-
- `@trypattern/sdk/schema`: `defineTables` and the types `Row`, `PublicRow` and `
|
|
80
|
+
- `@trypattern/sdk/schema`: `defineTables` and the types `Row`, `PublicRow`, `Seed` and `Starter`. Every row gets an
|
|
80
81
|
`id` and `createdAt` that Pattern sets. Every table declares `access` (`anyone`, `signedIn`,
|
|
81
82
|
`owner`, `admin` or `server` for read, create, update and delete) and `writes` (`optimistic` or
|
|
82
83
|
`confirmed`), and may add `owned: true` (an `ownerId` the server fills), `public` columns (a
|
|
@@ -84,7 +85,19 @@ export default createServer({ tables, admins })
|
|
|
84
85
|
own table of photos (`key`, `width`, `height`, `placeholder`, `alt`, `type`, `bytes`): an app
|
|
85
86
|
that shows photos adds it to its tables (`{ ...defineTables({ … }), media }`), binds an R2
|
|
86
87
|
bucket as `MEDIA` in `wrangler.jsonc` with `"/media/*"` in `run_worker_first`, and stores the
|
|
87
|
-
media row's `id` in its own columns. Anyone reads it; only the server creates rows.
|
|
88
|
+
media row's `id` in its own columns. Anyone reads it; only the server creates rows.
|
|
89
|
+
`bookingTables` are the bookings block's own tables (`bookingRules`, one row of weekday windows
|
|
90
|
+
`{ weekday, label, start, end, capacity }` with `leadTimeHours`, `daysAhead` and `autoAccept`;
|
|
91
|
+
`bookingBlackouts`, `{ date, reason }`; `bookingRequests`, `{ offering, date, window, name,
|
|
92
|
+
email, phone, address, notes, status }` with `status` `requested`, `accepted`, `declined` or
|
|
93
|
+
`cancelled`): an app that takes bookings spreads them into its tables
|
|
94
|
+
(`{ ...defineTables({ … }), ...bookingTables }`) so migrations, the seed and the dashboard's
|
|
95
|
+
database see them, and every operation on them is `server`. `seed.ts` is
|
|
96
|
+
sample data for dev and flows and never ships; `starter/` is the content a new site opens with,
|
|
97
|
+
written once on its first publish, then owned by the owner. `starter/rows.ts` exports
|
|
98
|
+
`starter: Starter<typeof tables>`: rows for tables that are not `owned`, and `media` rows as
|
|
99
|
+
`{ id, photo, alt }`, where `photo` names a file in `starter/photos/`. After that first publish
|
|
100
|
+
the folder is inert and may be deleted. `src/access.ts` is
|
|
88
101
|
the one rule engine; the Worker and the flow runner's memory API both use it, and
|
|
89
102
|
`accessFlows({ App })` from `@trypattern/sdk/flows` checks every table's rules against the seed.
|
|
90
103
|
Store writes return `'saved' | 'conflict' | 'forbidden' | 'missing' | 'failed'`. Safe to import
|
|
@@ -93,11 +106,15 @@ export default createServer({ tables, admins })
|
|
|
93
106
|
auth.trypattern.ai (Google, and emailed 6-digit codes; locally the code prints in the dev log),
|
|
94
107
|
dev sign-in, sessions, `/api/me` and every table under `/api/tables`. A deployed app holds one
|
|
95
108
|
`AUTH_SECRET` that Pattern's deploy sets, and no Google secrets. Also `users`, `sessions`,
|
|
96
|
-
With `media` in its tables it also serves `POST /api/media` (admins only,
|
|
97
|
-
|
|
109
|
+
With `media` in its tables it also serves `POST /api/media` (admins only, or Pattern with a
|
|
110
|
+
`media:write` assertion; a form with `file`, `alt` and an optional `id`, which makes a repeat
|
|
111
|
+
answer 200 with the row already there; up to 10 MB of JPEG, PNG, WebP, GIF or AVIF; HEIC is
|
|
112
|
+
refused): it reads the size from
|
|
98
113
|
the file's header (EXIF rotation included), stores it in `MEDIA` as `images/<sha256-16>.<ext>`,
|
|
99
114
|
asks the zone's image transformations for a 24-pixel JPEG placeholder (skipped in dev, so it is
|
|
100
|
-
`null` locally) and adds the row; and `GET /media/<key>`, immutable with an ETag
|
|
115
|
+
`null` locally) and adds the row; and `GET /media/<key>`, immutable with an ETag, which in dev
|
|
116
|
+
fetches a photo the local bucket lacks from `https://<name>.onpattern.app`, so a pulled project
|
|
117
|
+
shows the live photos without storing them. With
|
|
101
118
|
`pages` (`definePages(tables, { '/': head, '/items/:itemId': { head, paths } })` in
|
|
102
119
|
`server/pages.ts`) it renders the `<head>` of public pages: each `head` reads tables anyone may
|
|
103
120
|
read (`rows`, `row`, `photoUrl`) and returns `{ title, description, image, hero, jsonLd, data }`
|
|
@@ -108,7 +125,23 @@ export default createServer({ tables, admins })
|
|
|
108
125
|
and the alt as caption) and `robots.txt` come from the same list (`paths` lists a pattern's
|
|
109
126
|
params). It needs `"binding": "ASSETS"` and the page paths, `/sitemap.xml` and `/robots.txt` in
|
|
110
127
|
`run_worker_first`; other paths still get the built files. `localBusiness({ … })` and
|
|
111
|
-
`product({ … })` write the JSON-LD.
|
|
128
|
+
`product({ … })` write the JSON-LD. With
|
|
129
|
+
`blocks: [bookings({ timezone: 'America/Toronto' })]` (an IANA zone; the owner's days and
|
|
130
|
+
windows are in it) it serves the bookings block under `/api/bookings`, open to signed-out
|
|
131
|
+
visitors: `GET days?month=2026-11` (every day of the month as `open`, `closed` or `full`),
|
|
132
|
+
`GET windows?date=2026-11-13` (that day's bookable windows, `{ id, label, start, end,
|
|
133
|
+
remaining }`, the `id` being `start-end`), and `POST requests` (public; it checks again that the
|
|
134
|
+
day and window are bookable, then inserts only while the window's `requested` and `accepted`
|
|
135
|
+
requests are under its capacity, in one statement, so two visitors never both get the last place:
|
|
136
|
+
400 for a day or window that can't be booked, 409 when it just filled). A day is `closed` before
|
|
137
|
+
today, after `daysAhead`, on a blackout, or when none of its weekday's windows starts at least
|
|
138
|
+
`leadTimeHours` from now; `full` when every window left has no room. The owner's routes need an
|
|
139
|
+
admin: `GET requests`, `POST requests/:id/accept` (from `requested`), `POST requests/:id/decline`
|
|
140
|
+
(from `requested` or `accepted`, which frees the place), `GET rules` and `POST rules` (windows,
|
|
141
|
+
lead time, days ahead, `autoAccept` and the blackouts together). Emails to the owner and the
|
|
142
|
+
requester are not sent yet: `sendBookingEmail` in `src/server/bookings.ts` is the one place they
|
|
143
|
+
will go, and it prints each one in the dev log. `createServer` refuses a block whose tables are
|
|
144
|
+
missing from `tables`, and booking tables without the block. Also `users`, `sessions`,
|
|
112
145
|
`sqlTables`, `seedSql`, the sign-in wire schemas (`verifiedIdentity`, `exchangeRequest`,
|
|
113
146
|
`exchangeReply`, `sendCodeRequest`, `sendCodeReply`, `verifyCodeRequest`, `verifyCodeReply`) and
|
|
114
147
|
the Worker types `D1Database` and `R2Bucket`.
|
|
@@ -121,14 +154,21 @@ export default createServer({ tables, admins })
|
|
|
121
154
|
(`multiline` with `rows` for a few lines), `Select`, `Switch`,
|
|
122
155
|
`SegmentedControl`, `ChoiceGroup` (a wrapping radio group of `Choice` children, each with a
|
|
123
156
|
`title`, `subline`, `isDisabled` or any children such as an `Image` with a `label`; the chosen
|
|
124
|
-
one is ringed,
|
|
125
|
-
`Badge`, `Skeleton`, `Spinner`, `Icon`.
|
|
157
|
+
one is ringed, unchosen photos fade, arrow keys move between them), `Calendar` (a month grid of `open`, `closed` and `full` days, with month arrows and arrow-key movement), `Menu`, `ContextMenu`, `Dialog` (`size` up to `xl`, 800 pixels), `Tooltip`, `Kbd`, `ScrollArea`, `Avatar`,
|
|
158
|
+
`Badge`, `Skeleton`, `Spinner`, `Icon`. Dates: `Calendar` (a month grid; the caller owns the
|
|
159
|
+
month: `month="2026-11"` and `onMonthChange`, with `firstMonth` and `lastMonth` to stop the
|
|
160
|
+
arrows; `days` lists `{ date, state }` with `open`, `closed` (greyed) or `full` (struck
|
|
161
|
+
through), and a day it does not list is closed; `value` is filled and `onPick` gets an open day;
|
|
162
|
+
today, from the browser or `today`, is ringed; weeks start on Monday and the month and weekday
|
|
163
|
+
names follow `locale`; arrow keys move by day and week, Home and End to the week's ends, PageUp
|
|
164
|
+
and PageDown by month, moving the month with them). Photos: `Image` (`image` is a media row, `sizes` is
|
|
126
165
|
required, `priority` for the first photo on a page only, `aspect` crops with `fit=cover`): it
|
|
127
166
|
reserves the size from the row's `width` and `height`, offers 320 to 1920 pixel widths (never
|
|
128
167
|
wider than the original) through `/cdn-cgi/image/…` (plain `/media/<key>` on localhost), lazy-loads
|
|
129
168
|
unless `priority`, shows the blurred placeholder and fades the photo in over 200 ms unless it was
|
|
130
169
|
already loaded or motion is reduced, and shows a neutral block when the photo is missing or fails. Types: `IconName`, `MenuEntry`, `SidebarLink`,
|
|
131
|
-
`Toast`, `SelectOption`, `SegmentedOption`, `CommandMenuItem`, `SpaceName` and
|
|
170
|
+
`Toast`, `SelectOption`, `SegmentedOption`, `CommandMenuItem`, `CalendarDay`, `SpaceName` and
|
|
171
|
+
`Placement`.
|
|
132
172
|
Everything takes closed choices (`variant`, `tone`, `size`, `gap`, `width`) and a `style`
|
|
133
173
|
limited to placement, and every interactive primitive reports to `startApp`'s `onInteraction`.
|
|
134
174
|
App screens have no styles; `app check` holds them to that.
|
|
@@ -141,6 +181,22 @@ export default createServer({ tables, admins })
|
|
|
141
181
|
the app tsconfig preset loads, fills `Register` from `src/App.ts`, `schema.ts`,
|
|
142
182
|
`src/stores/index.ts` and `src/routes.tsx`. `Model`, `AppStores`, `AppSnapshot`, `AppCommand` and `AppPath` come from `@trypattern/sdk`.
|
|
143
183
|
|
|
184
|
+
- Blocks: an SDK module with its own tables, server routes and store, and no look; the app's
|
|
185
|
+
components draw it. The first is `bookings`, request-and-accept with windows and capacity:
|
|
186
|
+
`createServer({ tables, admins, blocks: [bookings({ timezone })] })` in `server/worker.ts` and
|
|
187
|
+
`defineApp({ tables, blocks: [bookings], stores, models, routes })` in `src/App.ts`, with
|
|
188
|
+
`...bookingTables` in `tables`. Every model then has `this.stores.bookings`, whose calls
|
|
189
|
+
(`bookings.days`, `bookings.windows`, `bookings.request`, `bookings.requests`, `bookings.accept`,
|
|
190
|
+
`bookings.decline`, `bookings.rules`, `bookings.saveRules`) go through the Gate:
|
|
191
|
+
`loadDays(month)` and `loadWindows(date)` fill `days[month]` and `windows[date]`;
|
|
192
|
+
`request({ offering, date, window, name, email, phone?, address?, notes? })` sets `sent` and
|
|
193
|
+
reloads that month and date; `loadRequests()` fills `requests` and `requestsStatus`;
|
|
194
|
+
`accept(id)`, `decline(id)`, `loadRules()` and `saveRules(rules)` keep `requests` and `rules`
|
|
195
|
+
current. Writes return `'saved' | 'conflict' | 'forbidden' | 'missing' | 'failed'` like table
|
|
196
|
+
writes. `offering` is any id the app gives it (an item, a size); the block never owns products.
|
|
197
|
+
In flows and `?memory` the calls fake against `world.bookings` (`BookingsWorld`: `now`,
|
|
198
|
+
`timezone`, `rules`, `requests`), with the same availability code as the Worker; the fake does
|
|
199
|
+
not check who is signed in. A type error names a block whose tables are not in `tables`.
|
|
144
200
|
- `@trypattern/sdk/flows`: `defineFlow({ App })`; the `flow()` it returns fakes the server
|
|
145
201
|
from the app's tables and the seed in `vite.config.ts`, and runs headless in Node or in Chrome with `app test --visual`.
|
|
146
202
|
- `@trypattern/sdk/vite`: `pattern({ tables, admins, seed })`, the whole Vite and Vitest setup, including the
|
|
@@ -151,7 +207,12 @@ export default createServer({ tables, admins })
|
|
|
151
207
|
and its icons. Pattern's dashboard renders the design system from it.
|
|
152
208
|
- `@trypattern/sdk/tsconfig/app.json` and `/tsconfig/server.json`: the presets project tsconfigs extend.
|
|
153
209
|
- The `app` command: start, check, fix, reset, migration, media (`app media add photo.jpg --alt "…"`
|
|
154
|
-
uploads to the dev server as the first admin
|
|
210
|
+
uploads to the dev server as the first admin; `--live` first uploads it to the live site through
|
|
211
|
+
onpattern.app, then locally under the same id), build, send, screenshot, test, sign-in, setup,
|
|
212
|
+
login, pull, publish, versions, restore. `app start` and `app reset` fill the local database
|
|
213
|
+
from `seed.ts` and `starter/rows.ts` and put `starter/photos` in the local `MEDIA` bucket;
|
|
214
|
+
`app publish --starter` sends `starter/` with the starter, and a new site's first publish adds
|
|
215
|
+
the photos through its own `POST /api/media`, then the rows.
|
|
155
216
|
|
|
156
217
|
Published to npm as `@trypattern/sdk`, compiled to `dist/`; in this repo the blueprint runs the
|
|
157
|
-
source directly. Still to come: payments, workflows and blocks.
|
|
218
|
+
source directly. Still to come: payments, workflows and more blocks.
|
package/dist/access.d.ts
CHANGED
|
@@ -16,10 +16,6 @@ export declare function viewerOf(user: {
|
|
|
16
16
|
id: string;
|
|
17
17
|
isAdmin: boolean;
|
|
18
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
19
|
export declare function readScope(table: Table, viewer: Viewer | null): ReadScope;
|
|
24
20
|
export declare function listRefusal(table: Table, viewer: Viewer | null): Refusal | undefined;
|
|
25
21
|
export declare function canSee(table: Table, viewer: Viewer | null, row: Owned): boolean;
|
package/dist/access.js
CHANGED
|
@@ -6,11 +6,6 @@ export function viewerOf(user) {
|
|
|
6
6
|
return null;
|
|
7
7
|
return { userId: user.id, isAdmin: user.isAdmin };
|
|
8
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
9
|
function mayUse(level, viewer) {
|
|
15
10
|
if (level === 'server')
|
|
16
11
|
return false;
|
package/dist/api.d.ts
CHANGED
|
@@ -24,7 +24,9 @@ export declare function memoryApi<Group extends Tables>(tables: Group, seed: See
|
|
|
24
24
|
admins: readonly string[];
|
|
25
25
|
title: string;
|
|
26
26
|
} & ServicesSetup): AppApi<Group>;
|
|
27
|
-
export declare function browserApi<Group extends Tables>(tables: Group, seed: Seed<Group>, gate: Gate, { inMemory, admins, ...setup }: {
|
|
27
|
+
export declare function browserApi<Group extends Tables>(tables: Group, seed: Seed<Group>, gate: Gate, { inMemory, signedInAs, admins, title, ...setup }: {
|
|
28
28
|
inMemory: boolean;
|
|
29
|
+
signedInAs: string | null;
|
|
29
30
|
admins: readonly string[];
|
|
31
|
+
title: string;
|
|
30
32
|
} & ServicesSetup): AppApi<Group>;
|
package/dist/api.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { viewerOf } from "./access.js";
|
|
2
2
|
import { HttpSessionApi, MemorySessionApi, sessionCalls } from "./auth/sessionApi.js";
|
|
3
3
|
import { HttpAppInfoApi, MemoryAppInfoApi, appInfoCalls } from "./appInfo.js";
|
|
4
4
|
import { httpServices, memoryServices } from "./serviceClients.js";
|
|
5
5
|
import { serviceCallNames } from "./services.js";
|
|
6
|
-
import { ownerIdOf } from "./table.js";
|
|
7
6
|
import { httpTables, memoryTables, tableCalls } from "./tables.js";
|
|
8
7
|
export function apiCalls(tables, services = {}) {
|
|
9
8
|
return [
|
|
@@ -42,25 +41,8 @@ export function memoryApi(tables, seed, { gate, signedInAs, admins, title, ...se
|
|
|
42
41
|
services: memoryClient(setup, options)
|
|
43
42
|
};
|
|
44
43
|
}
|
|
45
|
-
export function browserApi(tables, seed, gate, { inMemory, admins, ...setup }) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
tables: inMemory
|
|
51
|
-
? memoryTables(tables, seed, () => {
|
|
52
|
-
const user = session.rememberedUser();
|
|
53
|
-
return viewerByEmail(user === undefined
|
|
54
|
-
? undefined
|
|
55
|
-
: { ...user, id: ownerIdOf(user.email) }, emails);
|
|
56
|
-
}, { latencyMs: 0, gate })
|
|
57
|
-
: httpTables(tables, gate, () => session.reportSignedOut()),
|
|
58
|
-
session,
|
|
59
|
-
app: new HttpAppInfoApi(gate),
|
|
60
|
-
services: inMemory
|
|
61
|
-
? memoryClient(setup, options)
|
|
62
|
-
: setup.services === undefined
|
|
63
|
-
? {}
|
|
64
|
-
: httpServices(setup.services, gate, () => session.reportSignedOut())
|
|
65
|
-
};
|
|
44
|
+
export function browserApi(tables, seed, gate, { inMemory, signedInAs, admins, title, ...setup }) {
|
|
45
|
+
return inMemory
|
|
46
|
+
? memoryApi(tables, seed, { gate, signedInAs, admins, title, ...setup })
|
|
47
|
+
: httpApi(tables, gate, setup);
|
|
66
48
|
}
|
package/dist/app.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { DataRouter } from 'react-router';
|
|
2
2
|
import type { AppApi } from './api.ts';
|
|
3
3
|
import { AppStore } from './appInfo.ts';
|
|
4
|
+
import type { AppBlock } from './blocks.ts';
|
|
5
|
+
import type { bookingRequests } from './bookings.ts';
|
|
6
|
+
import type { Bookings } from './bookingsStore.ts';
|
|
4
7
|
import { SessionStore } from './auth/sessionStore.ts';
|
|
5
8
|
import type { Cache } from './cache.ts';
|
|
6
9
|
import type { Clock } from './clock.ts';
|
|
@@ -70,15 +73,27 @@ export type PublicStores<Group extends Tables> = {
|
|
|
70
73
|
never
|
|
71
74
|
] ? never : `public${Capitalize<Key>}`]: PublicTableStore<Group[Key]>;
|
|
72
75
|
};
|
|
76
|
+
export type BlockStores<Group extends Tables> = [
|
|
77
|
+
typeof bookingRequests
|
|
78
|
+
] extends [Group[keyof Group]] ? {
|
|
79
|
+
bookings: Bookings;
|
|
80
|
+
} : Record<never, never>;
|
|
73
81
|
export type StoresOf<Group extends Tables, Custom, Item extends Action = Action> = {
|
|
74
82
|
session: SessionStore;
|
|
75
83
|
app: AppStore;
|
|
76
84
|
actions: ActionsStore<Item>;
|
|
77
85
|
toasts: Toasts;
|
|
78
86
|
page: PageStore;
|
|
79
|
-
} & TableStores<Group> & PublicStores<Group> & Custom;
|
|
80
|
-
|
|
87
|
+
} & TableStores<Group> & PublicStores<Group> & BlockStores<Group> & Custom;
|
|
88
|
+
type BlockTablesMissing<Name extends string> = StaticTypeError<`The ${Name} block keeps its rows in Pattern's own tables: add them to tables in schema.ts (for bookings, ...bookingTables from '@trypattern/sdk/schema'), then run app migration add_${Name}`>;
|
|
89
|
+
export type CheckedBlocks<Group extends Tables, Blocks extends readonly AppBlock[]> = {
|
|
90
|
+
[Key in keyof Blocks]: [
|
|
91
|
+
Blocks[Key]['tables'][keyof Blocks[Key]['tables']]
|
|
92
|
+
] extends [Group[keyof Group]] ? Blocks[Key] : BlockTablesMissing<Blocks[Key]['name']>;
|
|
93
|
+
};
|
|
94
|
+
export type AppDefinition<Group extends Tables, Custom extends StoreClasses, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Blocks extends readonly AppBlock[] = readonly []> = {
|
|
81
95
|
tables: Group;
|
|
96
|
+
blocks?: Blocks & CheckedBlocks<Group, Blocks>;
|
|
82
97
|
stores?: Custom;
|
|
83
98
|
models: {
|
|
84
99
|
[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]>;
|
|
@@ -105,12 +120,12 @@ export type AppSnapshotShape<Models, Stores, Page> = {
|
|
|
105
120
|
stores: Snapshots<Stores>;
|
|
106
121
|
};
|
|
107
122
|
type AppCommandOf<Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[]> = MessagesOf<Instances<BuiltInModels>> | MessagesOf<Instances<Classes>> | NavigateCommand<PathOf<Routes>> | PressKeyCommand;
|
|
108
|
-
export type DefinedApp<Group extends Tables, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Custom extends StoreClasses> = {
|
|
123
|
+
export type DefinedApp<Group extends Tables, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Custom extends StoreClasses, Blocks extends readonly AppBlock[] = readonly []> = {
|
|
109
124
|
new (router: DataRouter, dependencies: AppDependencies<AppApi<Group>>): RunningApp<AppCommandOf<Classes, Routes>, AppSnapshotShape<Instances<BuiltInModels & Classes>, StoresOf<Group, StoreInstances<Custom>>, PageOf<Routes>>> & {
|
|
110
125
|
readonly models: Instances<BuiltInModels & Classes>;
|
|
111
126
|
readonly stores: StoresOf<Group, StoreInstances<Custom>>;
|
|
112
127
|
pressKey(key: string): Promise<void> | undefined;
|
|
113
128
|
};
|
|
114
|
-
readonly definition: AppDefinition<Group, Custom, Classes, Routes>;
|
|
129
|
+
readonly definition: AppDefinition<Group, Custom, Classes, Routes, Blocks>;
|
|
115
130
|
};
|
|
116
|
-
export declare function defineApp<Group extends Tables, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Custom extends StoreClasses = Record<never, never
|
|
131
|
+
export declare function defineApp<Group extends Tables, Classes extends Record<string, ModelClassLike>, Routes extends readonly RouteDefinition[], Custom extends StoreClasses = Record<never, never>, const Blocks extends readonly AppBlock[] = readonly []>(definition: AppDefinition<Group, Custom, Classes, Routes, Blocks>): DefinedApp<Group, Classes, Routes, Custom, Blocks>;
|
package/dist/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { viewerOf } from "./access.js";
|
|
3
3
|
import { AppStore } from "./appInfo.js";
|
|
4
|
+
import { blockWiringProblem } from "./blocks.js";
|
|
4
5
|
import { SessionStore } from "./auth/sessionStore.js";
|
|
5
6
|
import { realClock } from "./clock.js";
|
|
6
7
|
import { ActionsStore } from "./actions.js";
|
|
@@ -54,7 +55,30 @@ const builtInModels = {
|
|
|
54
55
|
signIn: SignIn
|
|
55
56
|
};
|
|
56
57
|
const appPathInput = z.custom((value) => typeof value === 'string' && isRoutedPath(value), { error: 'Not a page in src/routes.tsx' });
|
|
58
|
+
function withBlocks(definition, blocks) {
|
|
59
|
+
if (blocks.length === 0)
|
|
60
|
+
return definition;
|
|
61
|
+
const services = {
|
|
62
|
+
...definition.services
|
|
63
|
+
};
|
|
64
|
+
const memory = { ...definition.memory };
|
|
65
|
+
for (const block of blocks) {
|
|
66
|
+
for (const group of Object.keys(block.services)) {
|
|
67
|
+
if (Object.hasOwn(services, group)) {
|
|
68
|
+
throw new Error(`The ${block.name} block serves the ${group} calls, and so does src/services/index.ts: give your own services another name.`);
|
|
69
|
+
}
|
|
70
|
+
services[group] = block.services[group] ?? {};
|
|
71
|
+
memory[group] = Reflect.get(block.memory, group);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ...definition, services, memory };
|
|
75
|
+
}
|
|
57
76
|
export function defineApp(definition) {
|
|
77
|
+
const blocks = definition.blocks ?? [];
|
|
78
|
+
const wiring = blockWiringProblem(definition.tables, blocks, 'app');
|
|
79
|
+
if (wiring !== undefined)
|
|
80
|
+
throw new Error(wiring);
|
|
81
|
+
const defined = withBlocks(definition, blocks);
|
|
58
82
|
const pages = new Set(pageIds(definition.routes));
|
|
59
83
|
registerRoutes(definition.routes);
|
|
60
84
|
if (Object.hasOwn(definition.models, 'app')) {
|
|
@@ -66,7 +90,7 @@ export function defineApp(definition) {
|
|
|
66
90
|
}
|
|
67
91
|
}
|
|
68
92
|
return class App {
|
|
69
|
-
static definition =
|
|
93
|
+
static definition = defined;
|
|
70
94
|
router;
|
|
71
95
|
models;
|
|
72
96
|
stores;
|
|
@@ -97,15 +121,20 @@ export function defineApp(definition) {
|
|
|
97
121
|
key,
|
|
98
122
|
new Store(cache.persisted(`stores.${key}`), api.services)
|
|
99
123
|
]));
|
|
124
|
+
const blockStores = Object.fromEntries(blocks.map((block) => [
|
|
125
|
+
block.name,
|
|
126
|
+
new block.Store(cache.persisted(`stores.${block.name}`), api.services)
|
|
127
|
+
]));
|
|
100
128
|
const toasts = new Toasts(cache.persisted('stores.toasts'), {});
|
|
101
129
|
const clash = clashOf([
|
|
102
130
|
['session', 'app', 'actions', 'toasts', 'page'],
|
|
103
131
|
Object.keys(data.tables),
|
|
104
132
|
Object.keys(data.publicViews),
|
|
133
|
+
Object.keys(blockStores),
|
|
105
134
|
Object.keys(custom)
|
|
106
135
|
]);
|
|
107
136
|
if (clash !== undefined) {
|
|
108
|
-
throw new Error(`Two stores are called "${clash}". The built-in session, app, actions, toasts and page stores, each table, each table's public view and each store in src/stores/index.ts need their own name.`);
|
|
137
|
+
throw new Error(`Two stores are called "${clash}". The built-in session, app, actions, toasts and page stores, each table, each table's public view, each block and each store in src/stores/index.ts need their own name.`);
|
|
109
138
|
}
|
|
110
139
|
const actions = new ActionsStore(this.#page, [...(definition.actions ?? []), ...builtInActions], [
|
|
111
140
|
...(definition.actionGroups ?? []),
|
|
@@ -119,6 +148,7 @@ export function defineApp(definition) {
|
|
|
119
148
|
page: this.#page,
|
|
120
149
|
...data.tables,
|
|
121
150
|
...data.publicViews,
|
|
151
|
+
...blockStores,
|
|
122
152
|
...custom
|
|
123
153
|
};
|
|
124
154
|
this.#session = session;
|
package/dist/blocks.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Persisted } from './state.ts';
|
|
2
|
+
import type { Services } from './services.ts';
|
|
3
|
+
import type { Tables } from './table.ts';
|
|
4
|
+
export type BlockStoreClass = new (persisted: Persisted, services: never) => {
|
|
5
|
+
readonly state: object;
|
|
6
|
+
readonly subscribe: (listener: () => void) => () => void;
|
|
7
|
+
};
|
|
8
|
+
export type AppBlock = {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly tables: Tables;
|
|
11
|
+
readonly services: Services;
|
|
12
|
+
readonly memory: object;
|
|
13
|
+
readonly Store: BlockStoreClass;
|
|
14
|
+
};
|
|
15
|
+
export declare function blockWiringProblem(tables: Tables, blocks: readonly {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly tables: Tables;
|
|
18
|
+
}[], side: 'server' | 'app'): string | undefined;
|
package/dist/blocks.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { bookingTables } from "./bookings.js";
|
|
2
|
+
const KNOWN_BLOCKS = [
|
|
3
|
+
{
|
|
4
|
+
name: 'bookings',
|
|
5
|
+
tables: bookingTables,
|
|
6
|
+
tablesExport: 'bookingTables',
|
|
7
|
+
server: "bookings({ timezone: 'America/Toronto' })"
|
|
8
|
+
}
|
|
9
|
+
];
|
|
10
|
+
function includesAll(tables, wanted) {
|
|
11
|
+
const present = new Set(Object.values(tables));
|
|
12
|
+
return Object.values(wanted).every((table) => present.has(table));
|
|
13
|
+
}
|
|
14
|
+
function includesAny(tables, wanted) {
|
|
15
|
+
const present = new Set(Object.values(tables));
|
|
16
|
+
return Object.values(wanted).some((table) => present.has(table));
|
|
17
|
+
}
|
|
18
|
+
export function blockWiringProblem(tables, blocks, side) {
|
|
19
|
+
for (const block of blocks) {
|
|
20
|
+
if (includesAll(tables, block.tables))
|
|
21
|
+
continue;
|
|
22
|
+
const known = KNOWN_BLOCKS.find((item) => item.name === block.name);
|
|
23
|
+
const name = known?.tablesExport ?? `the ${block.name} tables`;
|
|
24
|
+
return `The ${block.name} block keeps its rows in Pattern's own tables: add ...${name} from '@trypattern/sdk/schema' to tables in schema.ts, then run app migration add_${block.name}`;
|
|
25
|
+
}
|
|
26
|
+
for (const known of KNOWN_BLOCKS) {
|
|
27
|
+
if (!includesAny(tables, known.tables))
|
|
28
|
+
continue;
|
|
29
|
+
if (blocks.some((block) => block.name === known.name))
|
|
30
|
+
continue;
|
|
31
|
+
return side === 'server'
|
|
32
|
+
? `tables has the ${known.name} block's tables, so the server needs the block: add blocks: [${known.server}] from '@trypattern/sdk/server' to createServer in server/worker.ts`
|
|
33
|
+
: `tables has the ${known.name} block's tables, so the app needs the block: add blocks: [${known.name}] from '@trypattern/sdk' to defineApp in src/App.ts`;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|