@trypattern/sdk 0.1.9 → 0.1.11
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 +37 -8
- package/dist/app.d.ts +1 -0
- package/dist/app.js +4 -3
- package/dist/cli/app.js +119 -6
- package/dist/cli/publishing.js +2 -0
- package/dist/components/AppFrame.d.ts +4 -1
- package/dist/components/AppFrame.js +25 -7
- package/dist/components/Card.d.ts +7 -2
- package/dist/components/Card.js +59 -3
- package/dist/components/Checkbox.d.ts +10 -0
- package/dist/components/Checkbox.js +59 -0
- package/dist/components/Choice.d.ts +15 -0
- package/dist/components/Choice.js +80 -0
- package/dist/components/ChoiceGroup.d.ts +11 -0
- package/dist/components/ChoiceGroup.js +44 -0
- package/dist/components/Icon.d.ts +2 -1
- package/dist/components/Icon.js +2 -2
- package/dist/components/Image.d.ts +11 -0
- package/dist/components/Image.js +76 -0
- package/dist/components/ListRow.d.ts +3 -1
- package/dist/components/ListRow.js +18 -4
- package/dist/components/PlainScreen.d.ts +16 -0
- package/dist/components/PlainScreen.js +80 -0
- package/dist/components/PlainSidebar.d.ts +15 -0
- package/dist/components/PlainSidebar.js +179 -0
- package/dist/components/Screen.d.ts +3 -1
- package/dist/components/Screen.js +5 -1
- package/dist/components/ScrollArea.d.ts +2 -1
- package/dist/components/ScrollArea.js +7 -2
- package/dist/components/Text.d.ts +1 -1
- package/dist/components/Text.js +10 -4
- package/dist/components/TextField.d.ts +5 -1
- package/dist/components/TextField.js +47 -9
- package/dist/components/choiceContext.d.ts +4 -0
- package/dist/components/choiceContext.js +7 -0
- package/dist/components/frame.d.ts +2 -0
- package/dist/components/icons.d.ts +3 -0
- package/dist/components/icons.js +4 -1
- package/dist/components/interaction.d.ts +1 -1
- package/dist/entityStore.d.ts +1 -0
- package/dist/entityStore.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/media.d.ts +36 -0
- package/dist/media.js +74 -0
- package/dist/publishWire.d.ts +2 -0
- package/dist/publishWire.js +21 -1
- package/dist/react.d.ts +5 -0
- package/dist/react.js +4 -0
- package/dist/rules/checkApiRouting.js +1 -1
- package/dist/rules/lintPlugin.js +13 -4
- package/dist/rules/messages.d.ts +1 -0
- package/dist/rules/messages.js +1 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/server/createServer.d.ts +5 -3
- package/dist/server/createServer.js +32 -2
- package/dist/server/env.d.ts +3 -1
- package/dist/server/imageHeader.d.ts +10 -0
- package/dist/server/imageHeader.js +159 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.js +3 -0
- package/dist/server/jsonLd.d.ts +45 -0
- package/dist/server/jsonLd.js +88 -0
- package/dist/server/media.d.ts +16 -0
- package/dist/server/media.js +135 -0
- package/dist/server/pages.d.ts +72 -0
- package/dist/server/pages.js +301 -0
- package/dist/server/session.js +1 -1
- package/dist/server/tables.d.ts +3 -0
- package/dist/server/tables.js +23 -8
- package/dist/start.js +30 -2
- package/dist/table.d.ts +1 -0
- package/dist/table.js +8 -1
- package/dist/tables.d.ts +2 -1
- package/dist/tables.js +10 -1
- package/package.json +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { imageSources, mediaPath, transformsOn } from "../media.js";
|
|
2
|
+
import { readTitle } from "./settings.js";
|
|
3
|
+
import { readRow, readRows } from "./tables.js";
|
|
4
|
+
const SEGMENT = /^(:[A-Za-z_]\w*|[\w.~-]+)$/;
|
|
5
|
+
const OG_WIDTH = 1200;
|
|
6
|
+
const OG_HEIGHT = 630;
|
|
7
|
+
const SITEMAP_LIMIT = 1000;
|
|
8
|
+
const DATA_ID = 'pattern-data';
|
|
9
|
+
function segmentsOf(pattern) {
|
|
10
|
+
if (!pattern.startsWith('/')) {
|
|
11
|
+
throw new Error(`A page path starts with /: "${pattern}"`);
|
|
12
|
+
}
|
|
13
|
+
const segments = pattern.split('/').filter((segment) => segment !== '');
|
|
14
|
+
const wrong = segments.find((segment) => !SEGMENT.test(segment));
|
|
15
|
+
if (wrong !== undefined) {
|
|
16
|
+
throw new Error(`"${pattern}" is not a page path: use words and :params, like /items/:itemId`);
|
|
17
|
+
}
|
|
18
|
+
return segments;
|
|
19
|
+
}
|
|
20
|
+
export function definePages(tables, pages) {
|
|
21
|
+
return {
|
|
22
|
+
tables,
|
|
23
|
+
routes: Object.entries(pages).map(([pattern, definition]) => {
|
|
24
|
+
const page = definition;
|
|
25
|
+
return {
|
|
26
|
+
pattern,
|
|
27
|
+
segments: segmentsOf(pattern),
|
|
28
|
+
...(typeof page === 'function' ? { head: page } : page)
|
|
29
|
+
};
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function matchPage(pages, path) {
|
|
34
|
+
const parts = path
|
|
35
|
+
.split('/')
|
|
36
|
+
.filter((part) => part !== '')
|
|
37
|
+
.map((part) => {
|
|
38
|
+
try {
|
|
39
|
+
return decodeURIComponent(part);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return part;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
for (const route of pages.routes) {
|
|
46
|
+
if (route.segments.length !== parts.length)
|
|
47
|
+
continue;
|
|
48
|
+
const params = {};
|
|
49
|
+
const matches = route.segments.every((segment, index) => {
|
|
50
|
+
const part = parts[index] ?? '';
|
|
51
|
+
if (segment.startsWith(':')) {
|
|
52
|
+
params[segment.slice(1)] = part;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return segment === part;
|
|
56
|
+
});
|
|
57
|
+
if (matches)
|
|
58
|
+
return { route, params };
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
function readableTable(pages, name) {
|
|
63
|
+
const table = pages.tables[name];
|
|
64
|
+
if (table === undefined)
|
|
65
|
+
throw new Error(`There is no table "${name}"`);
|
|
66
|
+
if (table.access.read !== 'anyone') {
|
|
67
|
+
throw new Error(`Pages are public, so they read only tables anyone may read; "${name}" has read: '${table.access.read}'`);
|
|
68
|
+
}
|
|
69
|
+
return table;
|
|
70
|
+
}
|
|
71
|
+
function requestFor(c, pages, params) {
|
|
72
|
+
const url = new URL(c.req.url);
|
|
73
|
+
const read = new Map();
|
|
74
|
+
return {
|
|
75
|
+
url,
|
|
76
|
+
params,
|
|
77
|
+
rows: (name) => {
|
|
78
|
+
const table = readableTable(pages, name);
|
|
79
|
+
const found = read.get(name) ?? readRows(c.env.DB, table);
|
|
80
|
+
read.set(name, found);
|
|
81
|
+
return found;
|
|
82
|
+
},
|
|
83
|
+
row: (name, id) => readRow(c.env.DB, readableTable(pages, name), id),
|
|
84
|
+
photoUrl: (image) => `${url.origin}${mediaPath(image.key)}`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function escapeHtml(text) {
|
|
88
|
+
return text
|
|
89
|
+
.replaceAll('&', '&')
|
|
90
|
+
.replaceAll('<', '<')
|
|
91
|
+
.replaceAll('>', '>')
|
|
92
|
+
.replaceAll('"', '"')
|
|
93
|
+
.replaceAll("'", ''');
|
|
94
|
+
}
|
|
95
|
+
function scriptJson(value) {
|
|
96
|
+
return JSON.stringify(value)
|
|
97
|
+
.replaceAll('<', '\\u003c')
|
|
98
|
+
.replaceAll('
', '\\u2028')
|
|
99
|
+
.replaceAll('
', '\\u2029');
|
|
100
|
+
}
|
|
101
|
+
function meta(attribute, key, content) {
|
|
102
|
+
return `<meta ${attribute}="${key}" content="${escapeHtml(content)}">`;
|
|
103
|
+
}
|
|
104
|
+
function openGraphImage(url, image) {
|
|
105
|
+
if (!transformsOn(url.hostname)) {
|
|
106
|
+
return {
|
|
107
|
+
src: `${url.origin}${mediaPath(image.key)}`,
|
|
108
|
+
width: image.width,
|
|
109
|
+
height: image.height
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const options = `width=${OG_WIDTH},height=${OG_HEIGHT},quality=82,format=jpeg,fit=cover`;
|
|
113
|
+
return {
|
|
114
|
+
src: `${url.origin}/cdn-cgi/image/${options}${mediaPath(image.key)}`,
|
|
115
|
+
width: OG_WIDTH,
|
|
116
|
+
height: OG_HEIGHT
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function heroPreload(url, hero) {
|
|
120
|
+
const sources = imageSources(hero.image, {
|
|
121
|
+
aspect: hero.aspect,
|
|
122
|
+
transforms: transformsOn(url.hostname)
|
|
123
|
+
});
|
|
124
|
+
const attributes = [
|
|
125
|
+
'rel="preload"',
|
|
126
|
+
'as="image"',
|
|
127
|
+
'fetchpriority="high"',
|
|
128
|
+
`href="${escapeHtml(sources.src)}"`,
|
|
129
|
+
...(sources.srcSet === undefined
|
|
130
|
+
? []
|
|
131
|
+
: [
|
|
132
|
+
`imagesrcset="${escapeHtml(sources.srcSet)}"`,
|
|
133
|
+
`imagesizes="${escapeHtml(hero.sizes)}"`
|
|
134
|
+
])
|
|
135
|
+
];
|
|
136
|
+
return `<link ${attributes.join(' ')}>`;
|
|
137
|
+
}
|
|
138
|
+
function dataOf(pages, head) {
|
|
139
|
+
return Object.fromEntries(Object.entries(head.data ?? {}).flatMap(([name, rows]) => {
|
|
140
|
+
if (rows === undefined)
|
|
141
|
+
return [];
|
|
142
|
+
readableTable(pages, name);
|
|
143
|
+
return [[name, rows]];
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
function headTags(pages, url, siteName, head) {
|
|
147
|
+
const canonical = `${url.origin}${url.pathname}`;
|
|
148
|
+
const image = head.image ?? head.hero?.image;
|
|
149
|
+
const og = image === undefined || image === null
|
|
150
|
+
? undefined
|
|
151
|
+
: openGraphImage(url, image);
|
|
152
|
+
const jsonLd = head.jsonLd === undefined
|
|
153
|
+
? []
|
|
154
|
+
: Array.isArray(head.jsonLd)
|
|
155
|
+
? head.jsonLd
|
|
156
|
+
: [head.jsonLd];
|
|
157
|
+
return [
|
|
158
|
+
...(head.description === undefined
|
|
159
|
+
? []
|
|
160
|
+
: [
|
|
161
|
+
meta('name', 'description', head.description),
|
|
162
|
+
meta('property', 'og:description', head.description)
|
|
163
|
+
]),
|
|
164
|
+
`<link rel="canonical" href="${escapeHtml(canonical)}">`,
|
|
165
|
+
meta('property', 'og:type', 'website'),
|
|
166
|
+
meta('property', 'og:title', head.title),
|
|
167
|
+
meta('property', 'og:url', canonical),
|
|
168
|
+
...(siteName === '' ? [] : [meta('property', 'og:site_name', siteName)]),
|
|
169
|
+
...(og === undefined || image === undefined || image === null
|
|
170
|
+
? [meta('name', 'twitter:card', 'summary')]
|
|
171
|
+
: [
|
|
172
|
+
meta('property', 'og:image', og.src),
|
|
173
|
+
meta('property', 'og:image:width', String(og.width)),
|
|
174
|
+
meta('property', 'og:image:height', String(og.height)),
|
|
175
|
+
meta('property', 'og:image:alt', image.alt),
|
|
176
|
+
meta('name', 'twitter:card', 'summary_large_image')
|
|
177
|
+
]),
|
|
178
|
+
...(head.hero === undefined || head.hero === null
|
|
179
|
+
? []
|
|
180
|
+
: [heroPreload(url, head.hero)]),
|
|
181
|
+
...jsonLd.map((item) => `<script type="application/ld+json">${scriptJson(item)}</script>`),
|
|
182
|
+
`<script type="application/json" id="${DATA_ID}">${scriptJson(dataOf(pages, head))}</script>`
|
|
183
|
+
].join('\n ');
|
|
184
|
+
}
|
|
185
|
+
const TITLE = /<title>[\s\S]*?<\/title>/;
|
|
186
|
+
const DESCRIPTION = /\s*<meta\s+name="description"[^>]*>/gi;
|
|
187
|
+
function rewrite(shell, title, tags) {
|
|
188
|
+
const titleTag = `<title>${escapeHtml(title)}</title>`;
|
|
189
|
+
const titled = TITLE.test(shell)
|
|
190
|
+
? shell.replace(TITLE, titleTag)
|
|
191
|
+
: shell.replace('</head>', ` ${titleTag}\n </head>`);
|
|
192
|
+
return titled
|
|
193
|
+
.replace(DESCRIPTION, '')
|
|
194
|
+
.replace('</head>', ` ${tags}\n </head>`);
|
|
195
|
+
}
|
|
196
|
+
async function shellOf(c) {
|
|
197
|
+
const assets = c.env.ASSETS;
|
|
198
|
+
if (assets === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
const response = await assets.fetch(new URL('/', c.req.url).href);
|
|
201
|
+
return response.ok ? response.text() : undefined;
|
|
202
|
+
}
|
|
203
|
+
const HTML = { 'content-type': 'text/html; charset=utf-8' };
|
|
204
|
+
export function wantsHtml(c) {
|
|
205
|
+
return (c.req.method === 'GET' &&
|
|
206
|
+
(c.req.header('sec-fetch-dest') === 'document' ||
|
|
207
|
+
(c.req.header('accept') ?? '').includes('text/html')));
|
|
208
|
+
}
|
|
209
|
+
export async function renderPage(c, pages, route, params) {
|
|
210
|
+
const shell = await shellOf(c);
|
|
211
|
+
if (shell === undefined) {
|
|
212
|
+
return c.text('Pages need the built index.html: add "binding": "ASSETS" under "assets" in wrangler.jsonc', 500);
|
|
213
|
+
}
|
|
214
|
+
const request = requestFor(c, pages, params);
|
|
215
|
+
let head;
|
|
216
|
+
let tags = '';
|
|
217
|
+
try {
|
|
218
|
+
head = await route.head(request);
|
|
219
|
+
if (head !== null) {
|
|
220
|
+
tags = headTags(pages, request.url, await readTitle(c.env.DB), head);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (import.meta.env.DEV)
|
|
225
|
+
throw error;
|
|
226
|
+
console.error(`Page ${route.pattern} failed:`, error);
|
|
227
|
+
return new Response(shell, { headers: HTML });
|
|
228
|
+
}
|
|
229
|
+
if (head === null) {
|
|
230
|
+
const missing = rewrite(shell, 'Not found', `${meta('name', 'robots', 'noindex')}\n <script type="application/json" id="${DATA_ID}">{}</script>`);
|
|
231
|
+
return new Response(missing, { status: 404, headers: HTML });
|
|
232
|
+
}
|
|
233
|
+
return new Response(rewrite(shell, head.title, tags), { headers: HTML });
|
|
234
|
+
}
|
|
235
|
+
function fill(route, params) {
|
|
236
|
+
const parts = route.segments.map((segment) => segment.startsWith(':') ? params[segment.slice(1)] : segment);
|
|
237
|
+
if (parts.some((part) => part === undefined || part === ''))
|
|
238
|
+
return undefined;
|
|
239
|
+
return `/${parts.map((part) => encodeURIComponent(part ?? '')).join('/')}`;
|
|
240
|
+
}
|
|
241
|
+
async function sitemapEntries(c, pages) {
|
|
242
|
+
const entries = [];
|
|
243
|
+
for (const route of pages.routes) {
|
|
244
|
+
const isStatic = route.segments.every((segment) => !segment.startsWith(':'));
|
|
245
|
+
const listed = isStatic
|
|
246
|
+
? [{}]
|
|
247
|
+
: route.paths === undefined
|
|
248
|
+
? []
|
|
249
|
+
: await route.paths(requestFor(c, pages, {}));
|
|
250
|
+
for (const params of listed) {
|
|
251
|
+
const path = fill(route, params);
|
|
252
|
+
if (path !== undefined)
|
|
253
|
+
entries.push({ path, route, params });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return entries.slice(0, SITEMAP_LIMIT);
|
|
257
|
+
}
|
|
258
|
+
export async function sitemap(c, pages) {
|
|
259
|
+
const origin = new URL(c.req.url).origin;
|
|
260
|
+
const urls = [];
|
|
261
|
+
for (const { path, route, params } of await sitemapEntries(c, pages)) {
|
|
262
|
+
const request = requestFor(c, pages, params);
|
|
263
|
+
const head = await (async () => route.head(request))().catch(() => null);
|
|
264
|
+
if (head === null)
|
|
265
|
+
continue;
|
|
266
|
+
const image = head.image ?? head.hero?.image;
|
|
267
|
+
urls.push([
|
|
268
|
+
' <url>',
|
|
269
|
+
` <loc>${escapeHtml(`${origin}${path}`)}</loc>`,
|
|
270
|
+
...(image === undefined || image === null
|
|
271
|
+
? []
|
|
272
|
+
: [
|
|
273
|
+
' <image:image>',
|
|
274
|
+
` <image:loc>${escapeHtml(request.photoUrl(image))}</image:loc>`,
|
|
275
|
+
` <image:caption>${escapeHtml(image.alt)}</image:caption>`,
|
|
276
|
+
' </image:image>'
|
|
277
|
+
]),
|
|
278
|
+
' </url>'
|
|
279
|
+
].join('\n'));
|
|
280
|
+
}
|
|
281
|
+
const xml = [
|
|
282
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
283
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">',
|
|
284
|
+
...urls,
|
|
285
|
+
'</urlset>',
|
|
286
|
+
''
|
|
287
|
+
].join('\n');
|
|
288
|
+
return new Response(xml, {
|
|
289
|
+
headers: { 'content-type': 'application/xml; charset=utf-8' }
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
export function robots(c) {
|
|
293
|
+
const origin = new URL(c.req.url).origin;
|
|
294
|
+
return new Response([
|
|
295
|
+
'User-agent: *',
|
|
296
|
+
'Allow: /',
|
|
297
|
+
'Disallow: /api/',
|
|
298
|
+
`Sitemap: ${origin}/sitemap.xml`,
|
|
299
|
+
''
|
|
300
|
+
].join('\n'), { headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
301
|
+
}
|
package/dist/server/session.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function sessionCookieFor(env, user) {
|
|
|
43
43
|
email: user.email,
|
|
44
44
|
recheckAt: nowInSeconds() + RECHECK_SECONDS
|
|
45
45
|
};
|
|
46
|
-
const cookie = await serializeSigned(SESSION_COOKIE, JSON.stringify(pass), env.SESSION_SECRET);
|
|
46
|
+
const cookie = await serializeSigned(SESSION_COOKIE, JSON.stringify(pass), env.SESSION_SECRET, { path: '/', secure: true });
|
|
47
47
|
return { cookie: cookie.split(';')[0] ?? cookie, sessionId };
|
|
48
48
|
}
|
|
49
49
|
export async function readPass(c) {
|
package/dist/server/tables.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
2
3
|
import type { ServerEnv } from './env.ts';
|
|
3
4
|
import type { Seed, Table, Tables } from '../table.ts';
|
|
4
5
|
type SqlTable = SQLiteTableWithColumns<{
|
|
@@ -10,6 +11,8 @@ type SqlTable = SQLiteTableWithColumns<{
|
|
|
10
11
|
export declare function sqlTable(table: Table): SqlTable;
|
|
11
12
|
export declare function sqlTables<Group extends Tables>(tables: Group): Record<keyof Group, SqlTable>;
|
|
12
13
|
export declare function seedSql<Group extends Tables>(tables: Group, seed: Seed<Group>): string;
|
|
14
|
+
export declare function readRows(d1: D1Database, table: Table): Promise<readonly Record<string, unknown>[]>;
|
|
15
|
+
export declare function readRow(d1: D1Database, table: Table, id: string): Promise<Record<string, unknown> | undefined>;
|
|
13
16
|
export declare function serveTables<Group extends Tables>(tables: Group): import("hono/hono-base").HonoBase<ServerEnv, {
|
|
14
17
|
"/:table": {
|
|
15
18
|
$get: {
|
package/dist/server/tables.js
CHANGED
|
@@ -99,6 +99,29 @@ export function seedSql(tables, seed) {
|
|
|
99
99
|
});
|
|
100
100
|
return [...users, ...rowsSql].join('\n');
|
|
101
101
|
}
|
|
102
|
+
function selection(table, target) {
|
|
103
|
+
return Object.fromEntries([
|
|
104
|
+
'id',
|
|
105
|
+
'createdAt',
|
|
106
|
+
...(table.owned ? ['ownerId'] : []),
|
|
107
|
+
...Object.keys(table.columns)
|
|
108
|
+
].map((key) => [key, column(target, key)]));
|
|
109
|
+
}
|
|
110
|
+
export async function readRows(d1, table) {
|
|
111
|
+
const target = sqlTable(table);
|
|
112
|
+
return drizzle(d1)
|
|
113
|
+
.select(selection(table, target))
|
|
114
|
+
.from(target)
|
|
115
|
+
.orderBy(asc(column(target, 'createdAt')), asc(column(target, 'id')));
|
|
116
|
+
}
|
|
117
|
+
export async function readRow(d1, table, id) {
|
|
118
|
+
const target = sqlTable(table);
|
|
119
|
+
return drizzle(d1)
|
|
120
|
+
.select(selection(table, target))
|
|
121
|
+
.from(target)
|
|
122
|
+
.where(eq(column(target, 'id'), id))
|
|
123
|
+
.get();
|
|
124
|
+
}
|
|
102
125
|
const REFUSAL_STATUSES = {
|
|
103
126
|
signedOut: { status: 401, error: 'Signed out' },
|
|
104
127
|
forbidden: { status: 403, error: 'forbidden' },
|
|
@@ -120,14 +143,6 @@ export function serveTables(tables) {
|
|
|
120
143
|
table.name,
|
|
121
144
|
{ table, target: sqlTable(table) }
|
|
122
145
|
]));
|
|
123
|
-
function selection(table, target) {
|
|
124
|
-
return Object.fromEntries([
|
|
125
|
-
'id',
|
|
126
|
-
'createdAt',
|
|
127
|
-
...(table.owned ? ['ownerId'] : []),
|
|
128
|
-
...Object.keys(table.columns)
|
|
129
|
-
].map((key) => [key, column(target, key)]));
|
|
130
|
-
}
|
|
131
146
|
function publicSelection(table, target) {
|
|
132
147
|
return Object.fromEntries(['id', ...table.public].map((key) => [key, column(target, key)]));
|
|
133
148
|
}
|
package/dist/start.js
CHANGED
|
@@ -14,6 +14,7 @@ import { AppProvider } from "./hooks/appContext.js";
|
|
|
14
14
|
import { routeObjects } from "./routes.js";
|
|
15
15
|
import { borders, breakpoints, colors, controls, fontFeatures, fontSizes, fontWeights, fonts, iconSizes, lineHeights, radii, shadows, space, widths } from "./theme/tokens.stylex.js";
|
|
16
16
|
const TEST_SEED = 'patternSeed';
|
|
17
|
+
const INLINE_DATA_ID = 'pattern-data';
|
|
17
18
|
const TEST_ADMINS = 'patternAdmins';
|
|
18
19
|
const TEST_WORLD = 'patternWorld';
|
|
19
20
|
function testSeed() {
|
|
@@ -26,6 +27,22 @@ function testAdmins() {
|
|
|
26
27
|
? admins.filter((email) => typeof email === 'string')
|
|
27
28
|
: [];
|
|
28
29
|
}
|
|
30
|
+
function readInlineData() {
|
|
31
|
+
const text = document.getElementById(INLINE_DATA_ID)?.textContent ?? '';
|
|
32
|
+
if (text === '')
|
|
33
|
+
return undefined;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(text);
|
|
36
|
+
return typeof parsed === 'object' &&
|
|
37
|
+
parsed !== null &&
|
|
38
|
+
!Array.isArray(parsed)
|
|
39
|
+
? parsed
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
29
46
|
export function startApp(App, { theme = [], icons = {}, onInteraction } = {}) {
|
|
30
47
|
setAppIcons(icons);
|
|
31
48
|
document.body.className = stylex.props(base.body, theme).className ?? '';
|
|
@@ -48,7 +65,8 @@ export function startApp(App, { theme = [], icons = {}, onInteraction } = {}) {
|
|
|
48
65
|
}),
|
|
49
66
|
cache: Cache.local(),
|
|
50
67
|
gate,
|
|
51
|
-
clock: inMemory ? manualClock() : realClock()
|
|
68
|
+
clock: inMemory ? manualClock() : realClock(),
|
|
69
|
+
inline: inMemory ? undefined : readInlineData()
|
|
52
70
|
});
|
|
53
71
|
if (hot)
|
|
54
72
|
exposeToDevTools(app, gate, hot, world);
|
|
@@ -81,12 +99,19 @@ function followAppearance(app) {
|
|
|
81
99
|
}
|
|
82
100
|
function followTitle(app) {
|
|
83
101
|
const store = app.stores.app;
|
|
102
|
+
let renderedPath = document.getElementById(INLINE_DATA_ID) === null
|
|
103
|
+
? undefined
|
|
104
|
+
: location.pathname;
|
|
84
105
|
const apply = () => {
|
|
106
|
+
if (location.pathname === renderedPath)
|
|
107
|
+
return;
|
|
108
|
+
renderedPath = undefined;
|
|
85
109
|
const { title } = store.currentState;
|
|
86
110
|
if (title !== '')
|
|
87
111
|
document.title = title;
|
|
88
112
|
};
|
|
89
113
|
store.subscribe(apply);
|
|
114
|
+
app.router.subscribe(apply);
|
|
90
115
|
apply();
|
|
91
116
|
}
|
|
92
117
|
function exposeToDevTools(app, gate, hot, world) {
|
|
@@ -140,6 +165,9 @@ const base = stylex.create({
|
|
|
140
165
|
[breakpoints.compact]: lineHeights.body
|
|
141
166
|
},
|
|
142
167
|
color: colors.primaryForeground,
|
|
143
|
-
backgroundColor: colors.shell
|
|
168
|
+
backgroundColor: colors.shell,
|
|
169
|
+
WebkitFontSmoothing: 'antialiased',
|
|
170
|
+
MozOsxFontSmoothing: 'grayscale',
|
|
171
|
+
fontSynthesis: 'none'
|
|
144
172
|
}
|
|
145
173
|
});
|
package/dist/table.d.ts
CHANGED
|
@@ -137,6 +137,7 @@ type CheckedTable<Definition> = {
|
|
|
137
137
|
export declare function defineTables<const Definitions extends Record<string, DraftTable> = Record<never, never>>(definitions: {
|
|
138
138
|
[Key in keyof Definitions]: CheckedTable<Definitions[Key]>;
|
|
139
139
|
}): TablesOf<Definitions>;
|
|
140
|
+
export declare function patternTable<const Name extends string, const Definition extends TableDefinition>(name: Name, definition: Definition): TableOf<Name, Definition>;
|
|
140
141
|
export declare function ownerIdOf(email: string): string;
|
|
141
142
|
export declare function seededRows(seed: Seed<Tables>, name: string): readonly Row<Table>[];
|
|
142
143
|
export declare function snakeCase(name: string): string;
|
package/dist/table.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
const NAME = /^[a-z][a-zA-Z0-9]*$/;
|
|
3
3
|
const RESERVED = ['id', 'createdAt', 'ownerId'];
|
|
4
4
|
const RESERVED_TABLES = ['users', 'sessions', 'session'];
|
|
5
|
+
const SDK_TABLES = ['media'];
|
|
5
6
|
const SEEDED_AT = Date.UTC(2000, 0, 1);
|
|
6
7
|
function problemIn(name, definition) {
|
|
7
8
|
const { access, columns } = definition;
|
|
@@ -28,7 +29,7 @@ function problemIn(name, definition) {
|
|
|
28
29
|
}
|
|
29
30
|
return undefined;
|
|
30
31
|
}
|
|
31
|
-
function table(name, definition) {
|
|
32
|
+
function table(name, definition, isPatterns = false) {
|
|
32
33
|
const { columns } = definition;
|
|
33
34
|
for (const key of [name, ...Object.keys(columns)]) {
|
|
34
35
|
if (!NAME.test(key)) {
|
|
@@ -38,6 +39,9 @@ function table(name, definition) {
|
|
|
38
39
|
if (RESERVED_TABLES.includes(name)) {
|
|
39
40
|
throw new Error(`A table can't be called "${name}": Pattern keeps its own ${name} for sign-in`);
|
|
40
41
|
}
|
|
42
|
+
if (!isPatterns && SDK_TABLES.includes(name)) {
|
|
43
|
+
throw new Error(`A table can't be called "${name}": Pattern has its own ${name} table. Add it with import { ${name} } from '@trypattern/sdk/schema'`);
|
|
44
|
+
}
|
|
41
45
|
const reserved = RESERVED.find((key) => key in columns);
|
|
42
46
|
if (reserved !== undefined) {
|
|
43
47
|
throw new Error(`Table "${name}" can't have a "${reserved}" column: Pattern adds it`);
|
|
@@ -75,6 +79,9 @@ function table(name, definition) {
|
|
|
75
79
|
export function defineTables(definitions) {
|
|
76
80
|
return Object.fromEntries(Object.entries(definitions).map(([name, definition]) => [name, table(name, definition)]));
|
|
77
81
|
}
|
|
82
|
+
export function patternTable(name, definition) {
|
|
83
|
+
return table(name, definition, true);
|
|
84
|
+
}
|
|
78
85
|
export function ownerIdOf(email) {
|
|
79
86
|
return `user-${email.toLowerCase()}`;
|
|
80
87
|
}
|
package/dist/tables.d.ts
CHANGED
|
@@ -64,10 +64,11 @@ type TableDeps<T extends Table> = {
|
|
|
64
64
|
publicView?: {
|
|
65
65
|
load: () => Promise<void>;
|
|
66
66
|
};
|
|
67
|
+
inline?: unknown;
|
|
67
68
|
};
|
|
68
69
|
export declare class TableStore<T extends Table> extends EntityStore<Row<T>> {
|
|
69
70
|
#private;
|
|
70
|
-
constructor(table: T, { api, cache, viewer, publicView }: TableDeps<T>);
|
|
71
|
+
constructor(table: T, { api, cache, viewer, publicView, inline }: TableDeps<T>);
|
|
71
72
|
get mayRead(): boolean;
|
|
72
73
|
load(): Promise<void>;
|
|
73
74
|
create(fields: Fields<T>): Promise<WriteOutcome>;
|
package/dist/tables.js
CHANGED
|
@@ -172,14 +172,23 @@ export class PublicTableStore extends StoreBase {
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
+
function inlineRows(table, inline) {
|
|
176
|
+
if (inline === undefined || table.access.read !== 'anyone')
|
|
177
|
+
return undefined;
|
|
178
|
+
const parsed = z.array(table.row).safeParse(inline);
|
|
179
|
+
return parsed.success
|
|
180
|
+
? parsed.data
|
|
181
|
+
: undefined;
|
|
182
|
+
}
|
|
175
183
|
export class TableStore extends EntityStore {
|
|
176
184
|
#table;
|
|
177
185
|
#viewer;
|
|
178
186
|
#publicView;
|
|
179
|
-
constructor(table, { api, cache, viewer, publicView }) {
|
|
187
|
+
constructor(table, { api, cache, viewer, publicView, inline }) {
|
|
180
188
|
super({
|
|
181
189
|
api,
|
|
182
190
|
writes: table.writes,
|
|
191
|
+
initial: inlineRows(table, inline),
|
|
183
192
|
cache: cache.scope(() => `${viewer()?.userId ?? 'anon'}:${table.name}`, z.array(table.row))
|
|
184
193
|
});
|
|
185
194
|
this.#table = table;
|