@trypattern/sdk 0.1.21 → 0.1.23
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/dist/cli/app.js +83 -1
- package/dist/cli/data.d.ts +5 -0
- package/dist/cli/data.js +273 -0
- package/dist/cli/publishing.d.ts +9 -2
- package/dist/cli/publishing.js +7 -7
- package/dist/cli/updates.d.ts +4 -0
- package/dist/cli/updates.js +68 -0
- package/dist/components/Badge.d.ts +1 -1
- package/dist/components/Badge.js +18 -0
- package/dist/components/ButtonBody.d.ts +1 -1
- package/dist/components/ButtonBody.js +43 -0
- package/dist/patternAccess.d.ts +2 -0
- package/dist/patternAccess.js +2 -1
- package/dist/publishWire.d.ts +355 -1
- package/dist/publishWire.js +66 -1
- package/dist/server/changes.d.ts +18 -0
- package/dist/server/changes.js +113 -0
- package/dist/server/createServer.d.ts +38 -0
- package/dist/server/database.d.ts +38 -0
- package/dist/server/database.js +2 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/patternViewer.js +4 -1
- package/dist/server/session.d.ts +1 -0
- package/dist/server/session.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { dataChange } from "../publishWire.js";
|
|
5
|
+
import { column, readRow, sqlTable } from "./tables.js";
|
|
6
|
+
const changeRequest = z.object({ changes: z.array(dataChange).max(1000) });
|
|
7
|
+
const META_KEYS = new Set(['id', 'createdAt', 'ownerId']);
|
|
8
|
+
function sameValue(left, right) {
|
|
9
|
+
return JSON.stringify(left ?? null) === JSON.stringify(right ?? null);
|
|
10
|
+
}
|
|
11
|
+
function changedSince(current, before, keys) {
|
|
12
|
+
return keys.some((key) => !sameValue(current[key], before[key]));
|
|
13
|
+
}
|
|
14
|
+
function withoutMeta(row) {
|
|
15
|
+
return Object.fromEntries(Object.entries(row).filter(([key]) => !META_KEYS.has(key)));
|
|
16
|
+
}
|
|
17
|
+
function failed(reason) {
|
|
18
|
+
return { status: 'failed', reason, row: null };
|
|
19
|
+
}
|
|
20
|
+
function skipped(reason) {
|
|
21
|
+
return { status: 'skipped', reason, row: null };
|
|
22
|
+
}
|
|
23
|
+
function problemOf(failure) {
|
|
24
|
+
for (let error = failure; error instanceof Error; error = error.cause) {
|
|
25
|
+
if (error.message.includes('UNIQUE constraint failed')) {
|
|
26
|
+
return 'Another row already has that value';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return 'The site couldn’t save this row';
|
|
30
|
+
}
|
|
31
|
+
async function applyOne(c, table, change) {
|
|
32
|
+
const target = sqlTable(table);
|
|
33
|
+
const db = drizzle(c.env.DB);
|
|
34
|
+
if (change.op === 'add') {
|
|
35
|
+
if (table.owned)
|
|
36
|
+
return failed('Rows in this table belong to their makers');
|
|
37
|
+
const id = typeof change.row['id'] === 'string' && change.row['id'] !== ''
|
|
38
|
+
? change.row['id']
|
|
39
|
+
: crypto.randomUUID();
|
|
40
|
+
const parsed = table.incoming.safeParse({ ...withoutMeta(change.row), id });
|
|
41
|
+
if (!parsed.success)
|
|
42
|
+
return failed(z.prettifyError(parsed.error));
|
|
43
|
+
const saved = await db
|
|
44
|
+
.insert(target)
|
|
45
|
+
.values(parsed.data)
|
|
46
|
+
.returning()
|
|
47
|
+
.get()
|
|
48
|
+
.catch((failure) => problemOf(failure));
|
|
49
|
+
if (typeof saved === 'string')
|
|
50
|
+
return failed(saved);
|
|
51
|
+
return {
|
|
52
|
+
status: 'applied',
|
|
53
|
+
reason: null,
|
|
54
|
+
row: (await readRow(c.env.DB, table, id)) ?? null
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const current = await readRow(c.env.DB, table, change.id);
|
|
58
|
+
if (current === undefined)
|
|
59
|
+
return skipped('It was removed since');
|
|
60
|
+
if (change.op === 'remove') {
|
|
61
|
+
if (changedSince(current, change.before, Object.keys(change.before))) {
|
|
62
|
+
return skipped('It changed since');
|
|
63
|
+
}
|
|
64
|
+
await db.delete(target).where(eq(column(target, 'id'), change.id));
|
|
65
|
+
return { status: 'applied', reason: null, row: current };
|
|
66
|
+
}
|
|
67
|
+
const keys = Object.keys(withoutMeta(change.after));
|
|
68
|
+
if (changedSince(current, change.before, keys)) {
|
|
69
|
+
return skipped('It changed since');
|
|
70
|
+
}
|
|
71
|
+
const parsed = table.incoming.safeParse({
|
|
72
|
+
...withoutMeta(current),
|
|
73
|
+
...withoutMeta(change.after),
|
|
74
|
+
id: change.id
|
|
75
|
+
});
|
|
76
|
+
if (!parsed.success)
|
|
77
|
+
return failed(z.prettifyError(parsed.error));
|
|
78
|
+
const values = Object.fromEntries(keys.map((key) => [key, Reflect.get(parsed.data, key)]));
|
|
79
|
+
const saved = await db
|
|
80
|
+
.update(target)
|
|
81
|
+
.set(values)
|
|
82
|
+
.where(eq(column(target, 'id'), change.id))
|
|
83
|
+
.run()
|
|
84
|
+
.then(() => undefined)
|
|
85
|
+
.catch((failure) => problemOf(failure));
|
|
86
|
+
if (saved !== undefined)
|
|
87
|
+
return failed(saved);
|
|
88
|
+
return {
|
|
89
|
+
status: 'applied',
|
|
90
|
+
reason: null,
|
|
91
|
+
row: (await readRow(c.env.DB, table, change.id)) ?? null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function applyChanges(c, tables) {
|
|
95
|
+
const viewer = c.get('viewer');
|
|
96
|
+
if (viewer === null)
|
|
97
|
+
return c.json({ error: 'Signed out' }, 401);
|
|
98
|
+
if (!viewer.isAdmin)
|
|
99
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
100
|
+
const body = changeRequest.safeParse(await c.req.json().catch(() => null));
|
|
101
|
+
if (!body.success) {
|
|
102
|
+
return c.json({ error: z.prettifyError(body.error) }, 400);
|
|
103
|
+
}
|
|
104
|
+
const known = new Map(Object.values(tables).map((table) => [table.name, table]));
|
|
105
|
+
const outcomes = [];
|
|
106
|
+
for (const change of body.data.changes) {
|
|
107
|
+
const table = known.get(change.table);
|
|
108
|
+
outcomes.push(table === undefined || table.access.read === 'server'
|
|
109
|
+
? failed('No such table')
|
|
110
|
+
: await applyOne(c, table, change));
|
|
111
|
+
}
|
|
112
|
+
return c.json({ outcomes });
|
|
113
|
+
}
|
|
@@ -213,6 +213,44 @@ export declare function createServer<Group extends Tables, Bindings extends Serv
|
|
|
213
213
|
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
214
214
|
};
|
|
215
215
|
};
|
|
216
|
+
} & {
|
|
217
|
+
"/changes": {
|
|
218
|
+
$post: {
|
|
219
|
+
input: {};
|
|
220
|
+
output: {
|
|
221
|
+
error: string;
|
|
222
|
+
};
|
|
223
|
+
outputFormat: "json";
|
|
224
|
+
status: 401;
|
|
225
|
+
} | {
|
|
226
|
+
input: {};
|
|
227
|
+
output: {
|
|
228
|
+
error: string;
|
|
229
|
+
};
|
|
230
|
+
outputFormat: "json";
|
|
231
|
+
status: 403;
|
|
232
|
+
} | {
|
|
233
|
+
input: {};
|
|
234
|
+
output: {
|
|
235
|
+
error: string;
|
|
236
|
+
};
|
|
237
|
+
outputFormat: "json";
|
|
238
|
+
status: 400;
|
|
239
|
+
} | {
|
|
240
|
+
input: {};
|
|
241
|
+
output: {
|
|
242
|
+
outcomes: {
|
|
243
|
+
status: "failed" | "applied" | "skipped";
|
|
244
|
+
reason: string | null;
|
|
245
|
+
row: {
|
|
246
|
+
[x: string]: import("hono/utils/types").JSONValue;
|
|
247
|
+
} | null;
|
|
248
|
+
}[];
|
|
249
|
+
};
|
|
250
|
+
outputFormat: "json";
|
|
251
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
252
|
+
};
|
|
253
|
+
};
|
|
216
254
|
} & {
|
|
217
255
|
"/tables/users": {
|
|
218
256
|
$get: {
|
|
@@ -39,6 +39,44 @@ export declare function serveDatabase<Group extends Tables>(tables: Group): impo
|
|
|
39
39
|
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
} & {
|
|
43
|
+
"/changes": {
|
|
44
|
+
$post: {
|
|
45
|
+
input: {};
|
|
46
|
+
output: {
|
|
47
|
+
error: string;
|
|
48
|
+
};
|
|
49
|
+
outputFormat: "json";
|
|
50
|
+
status: 401;
|
|
51
|
+
} | {
|
|
52
|
+
input: {};
|
|
53
|
+
output: {
|
|
54
|
+
error: string;
|
|
55
|
+
};
|
|
56
|
+
outputFormat: "json";
|
|
57
|
+
status: 403;
|
|
58
|
+
} | {
|
|
59
|
+
input: {};
|
|
60
|
+
output: {
|
|
61
|
+
error: string;
|
|
62
|
+
};
|
|
63
|
+
outputFormat: "json";
|
|
64
|
+
status: 400;
|
|
65
|
+
} | {
|
|
66
|
+
input: {};
|
|
67
|
+
output: {
|
|
68
|
+
outcomes: {
|
|
69
|
+
status: "failed" | "applied" | "skipped";
|
|
70
|
+
reason: string | null;
|
|
71
|
+
row: {
|
|
72
|
+
[x: string]: import("hono/utils/types").JSONValue;
|
|
73
|
+
} | null;
|
|
74
|
+
}[];
|
|
75
|
+
};
|
|
76
|
+
outputFormat: "json";
|
|
77
|
+
status: import("hono/utils/http-status").ContentfulStatusCode;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
42
80
|
} & {
|
|
43
81
|
"/tables/users": {
|
|
44
82
|
$get: {
|
package/dist/server/database.js
CHANGED
|
@@ -2,6 +2,7 @@ import { asc, count } from 'drizzle-orm';
|
|
|
2
2
|
import { drizzle } from 'drizzle-orm/d1';
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { applyChanges } from "./changes.js";
|
|
5
6
|
import { PAGE_PROBLEM, afterCursor, pageOf, pageReply } from "./keyset.js";
|
|
6
7
|
import { sqlTable } from "./tables.js";
|
|
7
8
|
import { users } from "./users.js";
|
|
@@ -66,6 +67,7 @@ export function serveDatabase(tables) {
|
|
|
66
67
|
};
|
|
67
68
|
return c.json({ tables: [...own, builtIn] });
|
|
68
69
|
})
|
|
70
|
+
.post('/changes', (c) => applyChanges(c, tables))
|
|
69
71
|
.get('/tables/users', async (c) => {
|
|
70
72
|
const refusal = adminRefusal(c);
|
|
71
73
|
if (refusal !== undefined)
|
package/dist/server/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export { seedSql, serveTables, sqlTable, sqlTables } from './tables.ts';
|
|
|
8
8
|
export { refused } from '../services.ts';
|
|
9
9
|
export { serveServices } from './services.ts';
|
|
10
10
|
export type { ServiceHandlers } from './services.ts';
|
|
11
|
-
export { sessionCookieFor } from './session.ts';
|
|
11
|
+
export { sessionCookieFor, sessionIdOf } from './session.ts';
|
|
12
12
|
export { deleteSession, sessions } from './sessions.ts';
|
|
13
13
|
export { readTitle, seedTitleSql, settings, writeTitle } from './settings.ts';
|
|
14
14
|
export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from './signInWire.ts';
|
package/dist/server/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { bookings } from "./bookings.js";
|
|
|
4
4
|
export { seedSql, serveTables, sqlTable, sqlTables } from "./tables.js";
|
|
5
5
|
export { refused } from "../services.js";
|
|
6
6
|
export { serveServices } from "./services.js";
|
|
7
|
-
export { sessionCookieFor } from "./session.js";
|
|
7
|
+
export { sessionCookieFor, sessionIdOf } from "./session.js";
|
|
8
8
|
export { deleteSession, sessions } from "./sessions.js";
|
|
9
9
|
export { readTitle, seedTitleSql, settings, writeTitle } from "./settings.js";
|
|
10
10
|
export { exchangeReply, exchangeRequest, sendCodeReply, sendCodeRequest, verifiedIdentity, verifyCodeReply, verifyCodeRequest } from "./signInWire.js";
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { MEDIA_WRITE, TABLES_READ, verifiedAssertion } from "../patternAccess.js";
|
|
1
|
+
import { MEDIA_WRITE, TABLES_READ, TABLES_WRITE, verifiedAssertion } from "../patternAccess.js";
|
|
2
2
|
import { PATTERN_KEYS } from "./patternKeys.js";
|
|
3
3
|
const BEARER = /^bearer\s+/i;
|
|
4
4
|
const SCHEMA_PATH = '/api/schema';
|
|
5
5
|
const TABLES_PATH = '/api/tables/';
|
|
6
6
|
const MEDIA_PATH = '/api/media';
|
|
7
|
+
const CHANGES_PATH = '/api/changes';
|
|
7
8
|
export function bearerOf(c) {
|
|
8
9
|
const header = c.req.header('authorization') ?? '';
|
|
9
10
|
return BEARER.test(header) ? header.replace(BEARER, '').trim() : undefined;
|
|
@@ -14,6 +15,8 @@ function scopeOf(c) {
|
|
|
14
15
|
(path === SCHEMA_PATH || path.startsWith(TABLES_PATH))) {
|
|
15
16
|
return TABLES_READ;
|
|
16
17
|
}
|
|
18
|
+
if (method === 'POST' && path === CHANGES_PATH)
|
|
19
|
+
return TABLES_WRITE;
|
|
17
20
|
return method === 'POST' && path === MEDIA_PATH ? MEDIA_WRITE : undefined;
|
|
18
21
|
}
|
|
19
22
|
export async function attachPatternViewer(c, next, token) {
|
package/dist/server/session.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare function readPass(c: Context<ServerEnv>): Promise<{
|
|
|
27
27
|
email: string;
|
|
28
28
|
recheckAt: number;
|
|
29
29
|
} | undefined>;
|
|
30
|
+
export declare function sessionIdOf<Env extends ServerEnv>(c: Context<Env>): Promise<string | undefined>;
|
|
30
31
|
export declare const needsRecheck: (pass: Pass) => boolean;
|
|
31
32
|
export declare function forgetSession(c: Context<ServerEnv>): void;
|
|
32
33
|
export {};
|
package/dist/server/session.js
CHANGED
|
@@ -58,6 +58,9 @@ export async function readPass(c) {
|
|
|
58
58
|
return undefined;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
export async function sessionIdOf(c) {
|
|
62
|
+
return (await readPass(c))?.sessionId;
|
|
63
|
+
}
|
|
61
64
|
export const needsRecheck = (pass) => pass.recheckAt <= nowInSeconds();
|
|
62
65
|
export function forgetSession(c) {
|
|
63
66
|
deleteCookie(c, SESSION_COOKIE, { path: '/', secure: true });
|