@trypattern/sdk 0.1.24 → 0.1.26
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/app.d.ts +2 -0
- package/dist/app.js +5 -2
- package/dist/builtins/Device.d.ts +15 -0
- package/dist/builtins/Device.js +52 -0
- package/dist/cli/app.js +53 -22
- package/dist/cli/startingValues.d.ts +5 -1
- package/dist/cli/startingValues.js +32 -42
- package/dist/registered.d.ts +2 -1
- package/dist/rules/checkAllFiles.js +1 -43
- package/dist/rules/messages.d.ts +2 -2
- package/dist/rules/messages.js +2 -2
- package/dist/server/tables.js +4 -3
- package/dist/store.d.ts +2 -1
- package/dist/table.d.ts +10 -1
- package/dist/table.js +10 -0
- package/package.json +1 -1
package/dist/app.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Clock } from './clock.ts';
|
|
|
10
10
|
import type { Gate } from './gate.ts';
|
|
11
11
|
import { ActionsStore } from './actions.ts';
|
|
12
12
|
import type { Action, ActionsBuilder } from './actions.ts';
|
|
13
|
+
import { Device } from './builtins/Device.ts';
|
|
13
14
|
import { Toasts } from './builtins/Toasts.ts';
|
|
14
15
|
import { PageStore } from './page.ts';
|
|
15
16
|
import type { BuiltInModels } from './registered.ts';
|
|
@@ -83,6 +84,7 @@ export type StoresOf<Group extends Tables, Custom, Item extends Action = Action>
|
|
|
83
84
|
app: AppStore;
|
|
84
85
|
actions: ActionsStore<Item>;
|
|
85
86
|
toasts: Toasts;
|
|
87
|
+
device: Device;
|
|
86
88
|
page: PageStore;
|
|
87
89
|
} & TableStores<Group> & PublicStores<Group> & BlockStores<Group> & Custom;
|
|
88
90
|
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}`>;
|
package/dist/app.js
CHANGED
|
@@ -10,6 +10,7 @@ import { AppSettings } from "./builtins/AppSettings.js";
|
|
|
10
10
|
import { BUILT_IN_GROUPS, builtInActions } from "./builtins/actions.js";
|
|
11
11
|
import { Palette } from "./builtins/Palette.js";
|
|
12
12
|
import { SignIn } from "./builtins/SignIn.js";
|
|
13
|
+
import { Device } from "./builtins/Device.js";
|
|
13
14
|
import { Toasts } from "./builtins/Toasts.js";
|
|
14
15
|
import { PageStore } from "./page.js";
|
|
15
16
|
import { assertNotBuilding, snapshotsOf } from "./store.js";
|
|
@@ -126,15 +127,16 @@ export function defineApp(definition) {
|
|
|
126
127
|
new block.Store(cache.persisted(`stores.${block.name}`), api.services)
|
|
127
128
|
]));
|
|
128
129
|
const toasts = new Toasts(cache.persisted('stores.toasts'), {});
|
|
130
|
+
const device = new Device(cache.persisted('stores.device'), {});
|
|
129
131
|
const clash = clashOf([
|
|
130
|
-
['session', 'app', 'actions', 'toasts', 'page'],
|
|
132
|
+
['session', 'app', 'actions', 'toasts', 'device', 'page'],
|
|
131
133
|
Object.keys(data.tables),
|
|
132
134
|
Object.keys(data.publicViews),
|
|
133
135
|
Object.keys(blockStores),
|
|
134
136
|
Object.keys(custom)
|
|
135
137
|
]);
|
|
136
138
|
if (clash !== undefined) {
|
|
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.`);
|
|
139
|
+
throw new Error(`Two stores are called "${clash}". The built-in session, app, actions, toasts, device and page stores, each table, each table's public view, each block and each store in src/stores/index.ts need their own name.`);
|
|
138
140
|
}
|
|
139
141
|
const actions = new ActionsStore(this.#page, [...(definition.actions ?? []), ...builtInActions], [
|
|
140
142
|
...(definition.actionGroups ?? []),
|
|
@@ -145,6 +147,7 @@ export function defineApp(definition) {
|
|
|
145
147
|
app,
|
|
146
148
|
actions,
|
|
147
149
|
toasts,
|
|
150
|
+
device,
|
|
148
151
|
page: this.#page,
|
|
149
152
|
...data.tables,
|
|
150
153
|
...data.publicViews,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Persisted } from '../state.ts';
|
|
3
|
+
declare const Device_base: import("../store.ts").StoreClass<{
|
|
4
|
+
today: z.ZodDefault<z.ZodString>;
|
|
5
|
+
isCompact: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
}, [], Record<never, never>>;
|
|
7
|
+
export declare class Device extends Device_base {
|
|
8
|
+
#private;
|
|
9
|
+
constructor(persisted: Persisted, services: Record<never, never>);
|
|
10
|
+
protected initialState(): {
|
|
11
|
+
today: string;
|
|
12
|
+
isCompact: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineStore } from "../store.js";
|
|
3
|
+
const Store = defineStore();
|
|
4
|
+
const COMPACT = '(max-width: 767px)';
|
|
5
|
+
function dayOf(at) {
|
|
6
|
+
const month = String(at.getMonth() + 1).padStart(2, '0');
|
|
7
|
+
const day = String(at.getDate()).padStart(2, '0');
|
|
8
|
+
return `${at.getFullYear()}-${month}-${day}`;
|
|
9
|
+
}
|
|
10
|
+
function msUntilTomorrow(at) {
|
|
11
|
+
const tomorrow = new Date(at);
|
|
12
|
+
tomorrow.setHours(24, 0, 1, 0);
|
|
13
|
+
return tomorrow.getTime() - at.getTime();
|
|
14
|
+
}
|
|
15
|
+
function compactQuery() {
|
|
16
|
+
return typeof window === 'undefined' || !('matchMedia' in window)
|
|
17
|
+
? undefined
|
|
18
|
+
: window.matchMedia(COMPACT);
|
|
19
|
+
}
|
|
20
|
+
export class Device extends Store({
|
|
21
|
+
state: {
|
|
22
|
+
today: z.string().default(''),
|
|
23
|
+
isCompact: z.boolean().default(false)
|
|
24
|
+
}
|
|
25
|
+
}) {
|
|
26
|
+
#midnight;
|
|
27
|
+
constructor(persisted, services) {
|
|
28
|
+
super(persisted, services);
|
|
29
|
+
const query = compactQuery();
|
|
30
|
+
if (query === undefined)
|
|
31
|
+
return;
|
|
32
|
+
query.addEventListener('change', () => this.set({ isCompact: query.matches }));
|
|
33
|
+
const refresh = () => this.#refreshToday();
|
|
34
|
+
window.addEventListener('focus', refresh);
|
|
35
|
+
document.addEventListener('visibilitychange', refresh);
|
|
36
|
+
this.#waitForMidnight();
|
|
37
|
+
}
|
|
38
|
+
initialState() {
|
|
39
|
+
return {
|
|
40
|
+
today: dayOf(new Date()),
|
|
41
|
+
isCompact: compactQuery()?.matches ?? false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
#refreshToday() {
|
|
45
|
+
this.set({ today: dayOf(new Date()) });
|
|
46
|
+
this.#waitForMidnight();
|
|
47
|
+
}
|
|
48
|
+
#waitForMidnight() {
|
|
49
|
+
clearTimeout(this.#midnight);
|
|
50
|
+
this.#midnight = setTimeout(() => this.#refreshToday(), msUntilTomorrow(new Date()));
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/cli/app.js
CHANGED
|
@@ -217,9 +217,9 @@ changing seed.ts or starter/. Works while the dev server runs; reload the page t
|
|
|
217
217
|
Compares the tables in schema.ts (plus Pattern's users, sessions and settings tables) with server/migrations and writes the
|
|
218
218
|
SQL that moves the database from one to the other, named <number>_<name>.sql. app start applies new
|
|
219
219
|
migrations to the local database. Prints "No schema changes" when the migrations already match.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
220
|
+
Rows that already exist (the live site has rows) need a value for a new required column, or for a
|
|
221
|
+
column that becomes required: give it one in schema.ts with defaults: { colour: 'white' } on the
|
|
222
|
+
table, or make it .nullable(). Without one this writes nothing and says which columns need it.`,
|
|
223
223
|
run: migration,
|
|
224
224
|
takesArgument: true
|
|
225
225
|
},
|
|
@@ -1299,38 +1299,66 @@ async function migration(name) {
|
|
|
1299
1299
|
'--name',
|
|
1300
1300
|
name
|
|
1301
1301
|
]);
|
|
1302
|
-
const
|
|
1302
|
+
const unstarted = code === 0 ? startNewColumns(name) : [];
|
|
1303
|
+
if (unstarted.length > 0) {
|
|
1304
|
+
const named = unstarted.map(({ table, column }) => `${camelCase(table)}.${camelCase(column)}`);
|
|
1305
|
+
const example = camelCase(unstarted[0]?.column ?? '');
|
|
1306
|
+
return {
|
|
1307
|
+
code: 1,
|
|
1308
|
+
data: { error: 'no starting value', columns: named },
|
|
1309
|
+
text: `✗ app migration ${name}: ${named.join(', ')} ${named.length === 1 ? 'is' : 'are'} required, and rows that already exist (the live site has rows) need a value. Nothing was written.\n next say what existing rows start with in schema.ts, on that table: defaults: { ${example}: … }, or make the column .nullable(). Then run app migration ${name} again.`
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1303
1312
|
const warning = code === 0 ? ownerBackfillWarning(name) : undefined;
|
|
1304
1313
|
return {
|
|
1305
1314
|
code,
|
|
1306
|
-
data: {
|
|
1307
|
-
|
|
1308
|
-
startingValues: notes,
|
|
1309
|
-
...(warning && { warning })
|
|
1310
|
-
},
|
|
1311
|
-
text: `${code === 0 ? '✓' : '✗'} app migration ${name}\n\n${indent(output.trimEnd())}${notes.map((note) => `\n note ${note}`).join('')}${warning === undefined ? '' : `\n\n! ${warning}`}`
|
|
1315
|
+
data: { output: output.trimEnd(), ...(warning && { warning }) },
|
|
1316
|
+
text: `${code === 0 ? '✓' : '✗'} app migration ${name}\n\n${indent(output.trimEnd())}${warning === undefined ? '' : `\n\n! ${warning}`}`
|
|
1312
1317
|
};
|
|
1313
1318
|
}
|
|
1314
|
-
function
|
|
1315
|
-
return
|
|
1316
|
-
.filter((entry) => entry.endsWith(`_${name}.sql`))
|
|
1317
|
-
.sort()
|
|
1318
|
-
.at(-1);
|
|
1319
|
+
function camelCase(name) {
|
|
1320
|
+
return name.replaceAll(/_([a-z0-9])/g, (_match, letter) => letter.toUpperCase());
|
|
1319
1321
|
}
|
|
1320
|
-
function
|
|
1322
|
+
function journalEntries() {
|
|
1321
1323
|
const journal = join(MIGRATIONS_DIR, 'meta', '_journal.json');
|
|
1322
1324
|
if (!existsSync(journal))
|
|
1323
|
-
return
|
|
1325
|
+
return undefined;
|
|
1326
|
+
const raw = JSON.parse(readText(journal));
|
|
1324
1327
|
const entries = z
|
|
1325
1328
|
.object({
|
|
1326
1329
|
entries: z.array(z.object({ idx: z.number(), tag: z.string() }))
|
|
1327
1330
|
})
|
|
1328
|
-
.parse(
|
|
1331
|
+
.parse(raw).entries;
|
|
1332
|
+
return { journal, raw, entries };
|
|
1333
|
+
}
|
|
1334
|
+
function snapshotPath(idx) {
|
|
1335
|
+
return join(MIGRATIONS_DIR, 'meta', `${String(idx).padStart(4, '0')}_snapshot.json`);
|
|
1336
|
+
}
|
|
1337
|
+
function forgetMigration(file) {
|
|
1338
|
+
rmSync(join(MIGRATIONS_DIR, file), { force: true });
|
|
1339
|
+
const found = journalEntries();
|
|
1340
|
+
const entry = found?.entries.find((item) => `${item.tag}.sql` === file);
|
|
1341
|
+
if (found === undefined || entry === undefined)
|
|
1342
|
+
return;
|
|
1343
|
+
rmSync(snapshotPath(entry.idx), { force: true });
|
|
1344
|
+
writeFileSync(found.journal, `${JSON.stringify({
|
|
1345
|
+
...Object(found.raw),
|
|
1346
|
+
entries: Reflect.get(Object(found.raw), 'entries').filter((item) => Reflect.get(Object(item), 'tag') !== entry.tag)
|
|
1347
|
+
}, null, 2)}\n`);
|
|
1348
|
+
}
|
|
1349
|
+
function newestMigration(name) {
|
|
1350
|
+
return readdirSync(MIGRATIONS_DIR)
|
|
1351
|
+
.filter((entry) => entry.endsWith(`_${name}.sql`))
|
|
1352
|
+
.sort()
|
|
1353
|
+
.at(-1);
|
|
1354
|
+
}
|
|
1355
|
+
function previousTables(file) {
|
|
1356
|
+
const entries = journalEntries()?.entries ?? [];
|
|
1329
1357
|
const at = entries.findIndex((entry) => `${entry.tag}.sql` === file);
|
|
1330
1358
|
const before = at > 0 ? entries[at - 1] : undefined;
|
|
1331
1359
|
if (before === undefined)
|
|
1332
1360
|
return {};
|
|
1333
|
-
const snapshot =
|
|
1361
|
+
const snapshot = snapshotPath(before.idx);
|
|
1334
1362
|
if (!existsSync(snapshot))
|
|
1335
1363
|
return {};
|
|
1336
1364
|
return z
|
|
@@ -1350,9 +1378,12 @@ function startNewColumns(name) {
|
|
|
1350
1378
|
table,
|
|
1351
1379
|
columns
|
|
1352
1380
|
]));
|
|
1353
|
-
const { sql,
|
|
1354
|
-
|
|
1355
|
-
|
|
1381
|
+
const { sql, unstarted } = withStartingValues(readText(path), tables);
|
|
1382
|
+
if (unstarted.length > 0)
|
|
1383
|
+
forgetMigration(file);
|
|
1384
|
+
else
|
|
1385
|
+
writeFileSync(path, sql);
|
|
1386
|
+
return unstarted;
|
|
1356
1387
|
}
|
|
1357
1388
|
function ownerBackfillWarning(name) {
|
|
1358
1389
|
const file = newestMigration(name);
|
|
@@ -2,8 +2,12 @@ type Columns = Readonly<Record<string, {
|
|
|
2
2
|
notNull: boolean;
|
|
3
3
|
}>>;
|
|
4
4
|
export type PreviousTables = Readonly<Record<string, Columns>>;
|
|
5
|
+
export type Unstarted = {
|
|
6
|
+
table: string;
|
|
7
|
+
column: string;
|
|
8
|
+
};
|
|
5
9
|
export declare function withStartingValues(sql: string, previous: PreviousTables): {
|
|
6
10
|
sql: string;
|
|
7
|
-
|
|
11
|
+
unstarted: Unstarted[];
|
|
8
12
|
};
|
|
9
13
|
export {};
|
|
@@ -1,58 +1,48 @@
|
|
|
1
|
-
const ADDED = /^ALTER TABLE `(\w+)` ADD `(\w+)` (text|integer|real) NOT NULL;/gm;
|
|
1
|
+
const ADDED = /^ALTER TABLE `(\w+)` ADD `(\w+)` (?:text|integer|real) NOT NULL;/gm;
|
|
2
2
|
const REBUILT = /CREATE TABLE `__new_(\w+)` \(([^;]*?)\n\);(\n(?:--> statement-breakpoint\n)?\n?)INSERT INTO `__new_\1`\(([^)]*)\) SELECT ([^;]*) FROM `\1`;/g;
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
text: "''",
|
|
6
|
-
integer: '0',
|
|
7
|
-
real: '0'
|
|
8
|
-
};
|
|
9
|
-
const SPOKEN = {
|
|
10
|
-
text: 'empty',
|
|
11
|
-
integer: '0 (or "no")',
|
|
12
|
-
real: '0'
|
|
13
|
-
};
|
|
3
|
+
const COLUMN = /^\s*`(\w+)` (?:text|integer|real)(.*)$/;
|
|
4
|
+
const STARTS_AS = / DEFAULT ('(?:[^']|'')*'|[-\w.]+)/;
|
|
14
5
|
function quoted(names) {
|
|
15
6
|
return names.split(',').map((name) => name.trim().replaceAll('"', ''));
|
|
16
7
|
}
|
|
8
|
+
function startsOf(body) {
|
|
9
|
+
const starts = new Map();
|
|
10
|
+
for (const line of body.split('\n')) {
|
|
11
|
+
const match = COLUMN.exec(line);
|
|
12
|
+
const rest = match?.[2] ?? '';
|
|
13
|
+
if (match?.[1] === undefined || !rest.includes('NOT NULL'))
|
|
14
|
+
continue;
|
|
15
|
+
if (rest.includes('PRIMARY KEY'))
|
|
16
|
+
continue;
|
|
17
|
+
starts.set(match[1], STARTS_AS.exec(rest)?.[1] ?? null);
|
|
18
|
+
}
|
|
19
|
+
return starts;
|
|
20
|
+
}
|
|
17
21
|
export function withStartingValues(sql, previous) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const unstarted = [
|
|
23
|
+
...Array.from(sql.matchAll(ADDED), ([, table = '', column = '']) => ({
|
|
24
|
+
table,
|
|
25
|
+
column
|
|
26
|
+
}))
|
|
27
|
+
];
|
|
28
|
+
const rebuilt = sql.replaceAll(REBUILT, (whole, table, body, between, into, from) => {
|
|
24
29
|
const old = previous[table];
|
|
25
30
|
if (old === undefined)
|
|
26
31
|
return whole;
|
|
27
|
-
const
|
|
28
|
-
const lines = body.split('\n').map((line) => {
|
|
29
|
-
const match = REQUIRED.exec(line);
|
|
30
|
-
if (match === null || line.includes('PRIMARY KEY'))
|
|
31
|
-
return line;
|
|
32
|
-
const [, start = '', column = '', type = '', comma = ''] = match;
|
|
33
|
-
types.set(column, type);
|
|
34
|
-
return `${start} NOT NULL DEFAULT ${STARTING_VALUES[type] ?? "''"}${comma}`;
|
|
35
|
-
});
|
|
32
|
+
const starts = startsOf(body);
|
|
36
33
|
const selected = quoted(from).map((column) => {
|
|
37
|
-
const type = types.get(column);
|
|
38
34
|
const before = old[column];
|
|
39
|
-
if (
|
|
35
|
+
if (!starts.has(column) || before?.notNull === true) {
|
|
40
36
|
return `"${column}"`;
|
|
41
|
-
const value = STARTING_VALUES[type] ?? "''";
|
|
42
|
-
if (before === undefined) {
|
|
43
|
-
started.push({ table, column, type });
|
|
44
|
-
return value;
|
|
45
37
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
const start = starts.get(column) ?? null;
|
|
39
|
+
if (start === null) {
|
|
40
|
+
unstarted.push({ table, column });
|
|
41
|
+
return `"${column}"`;
|
|
49
42
|
}
|
|
50
|
-
return `"${column}"`;
|
|
43
|
+
return before === undefined ? start : `coalesce("${column}", ${start})`;
|
|
51
44
|
});
|
|
52
|
-
return `CREATE TABLE \`__new_${table}\` (${
|
|
45
|
+
return `CREATE TABLE \`__new_${table}\` (${body}\n);${between}INSERT INTO \`__new_${table}\`(${into}) SELECT ${selected.join(', ')} FROM \`${table}\`;`;
|
|
53
46
|
});
|
|
54
|
-
return {
|
|
55
|
-
sql: rebuilt,
|
|
56
|
-
notes: started.map(({ table, column, type }) => `${table}.${column} starts as ${SPOKEN[type] ?? 'empty'} on rows that already exist. To start them on a real value, add UPDATE \`${table}\` SET \`${column}\` = … after it in the migration.`)
|
|
57
|
-
};
|
|
47
|
+
return { sql: rebuilt, unstarted };
|
|
58
48
|
}
|
package/dist/registered.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AppSnapshotShape, ModelServicesOf, NavigateCommand, PressKeyComman
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { Action, DefineActions } from './actions.ts';
|
|
4
4
|
import type { ServiceClient, Services } from './services.ts';
|
|
5
|
+
import type { StoreFactory } from './store.ts';
|
|
5
6
|
import type { Instances, MessagesOf, ModelClassLike, ModelSnapshots } from './model.ts';
|
|
6
7
|
import type { BUILT_IN_GROUPS } from './builtins/actions.ts';
|
|
7
8
|
import type { Account } from './builtins/Account.ts';
|
|
@@ -48,7 +49,7 @@ export type ActionContext = {
|
|
|
48
49
|
};
|
|
49
50
|
export type ModelServices = ModelServicesOf<AppPath>;
|
|
50
51
|
export declare const Model: <S extends import("./state.ts").StateSpec = Record<never, never>, const P extends readonly (keyof S & string)[] = []>(config?: import("./model.ts").ModelConfig<S, P>) => import("./model.ts").ModelClass<AppStores, ModelServices, S, P>;
|
|
51
|
-
export declare const Store: <
|
|
52
|
+
export declare const Store: StoreFactory<AppServices>;
|
|
52
53
|
export declare const defineActions: DefineActions<ActionContext, RegisteredAction, AppCommand>;
|
|
53
54
|
export declare const appPath: z.ZodCustom<AppPath, AppPath>;
|
|
54
55
|
type ActionInput = Pick<AppAction, 'id' | 'label' | 'group' | 'icon'> & Partial<Pick<AppAction, 'keys' | 'keywords' | 'isDanger' | 'isSearchOnly' | 'isHidden'>> & {
|
|
@@ -378,14 +378,6 @@ function misplacedSourceFiles() {
|
|
|
378
378
|
return ownedFiles(SRC_DIRECTORY).filter((path) => !isWhereItBelongs(relative(SRC_DIRECTORY, path)) || !isInItsFolder(path));
|
|
379
379
|
}
|
|
380
380
|
const SCHEMA_FILE = join(ROOT, 'schema.ts');
|
|
381
|
-
function rowTypeName(table) {
|
|
382
|
-
const one = table.endsWith('ies')
|
|
383
|
-
? `${table.slice(0, -3)}y`
|
|
384
|
-
: table.endsWith('s')
|
|
385
|
-
? table.slice(0, -1)
|
|
386
|
-
: table;
|
|
387
|
-
return `${one.charAt(0).toUpperCase()}${one.slice(1)}`;
|
|
388
|
-
}
|
|
389
381
|
function isExported(node) {
|
|
390
382
|
return (ts.canHaveModifiers(node) &&
|
|
391
383
|
(ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
|
|
@@ -403,8 +395,6 @@ function schemaFindings() {
|
|
|
403
395
|
return [];
|
|
404
396
|
const text = readFileSync(SCHEMA_FILE, 'utf8');
|
|
405
397
|
const source = ts.createSourceFile(SCHEMA_FILE, text, ts.ScriptTarget.Latest, true);
|
|
406
|
-
const tables = [];
|
|
407
|
-
const rowTypes = new Set();
|
|
408
398
|
let exportsTables = false;
|
|
409
399
|
let exportsAdmins = false;
|
|
410
400
|
const visit = (node) => {
|
|
@@ -422,29 +412,6 @@ function schemaFindings() {
|
|
|
422
412
|
definesTables(declaration.initializer, source))) {
|
|
423
413
|
exportsTables = true;
|
|
424
414
|
}
|
|
425
|
-
if (ts.isCallExpression(node) &&
|
|
426
|
-
node.expression.getText(source) === 'defineTables') {
|
|
427
|
-
const [definitions] = node.arguments;
|
|
428
|
-
if (definitions !== undefined &&
|
|
429
|
-
ts.isObjectLiteralExpression(definitions)) {
|
|
430
|
-
for (const property of definitions.properties) {
|
|
431
|
-
const name = property.name;
|
|
432
|
-
if (name !== undefined &&
|
|
433
|
-
(ts.isIdentifier(name) || ts.isStringLiteral(name))) {
|
|
434
|
-
tables.push({ key: name.text, offset: property.getStart(source) });
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (ts.isTypeAliasDeclaration(node) &&
|
|
440
|
-
isExported(node) &&
|
|
441
|
-
ts.isTypeReferenceNode(node.type) &&
|
|
442
|
-
node.type.typeName.getText(source) === 'Row') {
|
|
443
|
-
const [argument] = node.type.typeArguments ?? [];
|
|
444
|
-
if (argument !== undefined && ts.isTypeQueryNode(argument)) {
|
|
445
|
-
rowTypes.add(argument.exprName.getText(source));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
415
|
ts.forEachChild(node, visit);
|
|
449
416
|
};
|
|
450
417
|
visit(source);
|
|
@@ -456,18 +423,9 @@ function schemaFindings() {
|
|
|
456
423
|
}
|
|
457
424
|
];
|
|
458
425
|
}
|
|
459
|
-
|
|
426
|
+
return exportsAdmins
|
|
460
427
|
? []
|
|
461
428
|
: [{ offset: 0, fix: 'export const admins: string[] = []' }];
|
|
462
|
-
return [
|
|
463
|
-
...missingAdmins,
|
|
464
|
-
...tables
|
|
465
|
-
.filter(({ key }) => !rowTypes.has(`tables.${key}`))
|
|
466
|
-
.map(({ key, offset }) => ({
|
|
467
|
-
offset,
|
|
468
|
-
fix: `export type ${rowTypeName(key)} = Row<typeof tables.${key}>`
|
|
469
|
-
}))
|
|
470
|
-
];
|
|
471
429
|
}
|
|
472
430
|
const ACCESS_FLOWS_FILE = join(ROOT, 'flows', 'Access.flow.ts');
|
|
473
431
|
function toasterPlacements() {
|
package/dist/rules/messages.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare const DEV_ONLY_SIGN_IN_MESSAGE = "The server has exactly one dev-
|
|
|
22
22
|
export declare const STATE_VIA_HOOKS_MESSAGE = "A screen reads only its model: const issueList = useModel('issueList'), then issueList.issues, issueList.isLoading and issueList.createIssue(). Reading .state, .models, .stores or getSnapshot() directly does not subscribe, so the screen would show stale data. If the screen needs something from a store, add a getter for it to the model.";
|
|
23
23
|
export declare const CLASSES_NOT_FACTORIES_MESSAGE = "A function that returns an object of methods is a class in disguise: write it as a class, with private fields for its state and methods for its behaviour. One shape for everything that has state keeps the codebase predictable. Plain functions are for pure helpers that compute a value (sortProjects, newClientSideId).";
|
|
24
24
|
export declare const ACCESS_FLOWS_MESSAGE = "flows/Access.flow.ts runs Pattern's generated access flows, which prove every table's access rules against the seed on each app test. Keep it: `import { accessFlows } from '@trypattern/sdk/flows'` and `accessFlows({ App })`.";
|
|
25
|
-
export declare const SCHEMA_TABLES_MESSAGE = "schema.ts defines every saved table in one place and says who may do what: `export const tables = defineTables({ issues: { columns: { title: z.string() }, access: { read: 'signedIn', create: 'signedIn', update: 'signedIn', delete: 'signedIn' }, writes: 'optimistic' } })`.
|
|
25
|
+
export declare const SCHEMA_TABLES_MESSAGE = "schema.ts defines every saved table in one place and says who may do what: `export const tables = defineTables({ issues: { columns: { title: z.string() }, access: { read: 'signedIn', create: 'signedIn', update: 'signedIn', delete: 'signedIn' }, writes: 'optimistic' } })`. When another file needs a table's row type, schema.ts exports it, named for one row (`export type Issue = Row<typeof tables.issues>`). It also exports the emails of the people who run the app, `export const admins: string[] = []`, even when there are none yet, so the question is answered on purpose. Add the line shown.";
|
|
26
26
|
export declare const NO_UNUSED_EXPORTS_MESSAGE = "Export only what another file imports. Nothing imports this, so remove `export` (and the code itself if this file does not use it either). Unused exports hide dead code and make a file look like it offers more than it does. Pattern reads `tables` from schema.ts and `seed` from seed.ts, and vite.config.ts and server/worker.ts default-export their config, so those stay.";
|
|
27
27
|
export declare const NO_RE_EXPORTS_MESSAGE = "Do not pass imports through: a file exports only what it defines. Import the thing from where it lives instead (command from @trypattern/sdk, a component from its own file), so every import names the real source. server/worker.ts is the one exception: Cloudflare runs the Workflow and Durable Object classes its Worker exports, so it may export them from where they live.";
|
|
28
28
|
export declare const EXPLICIT_ACCESS_MESSAGE = "Mark every class member public or private (or protected, to override a base class member), so a reader sees at a glance what other code may call: `public setSort = command(...)`, `public override onPageOpen()`, `private loadIssues()`. Use the word private, never #name, so there is one way to say it. Constructors need neither.";
|
|
@@ -30,7 +30,7 @@ export declare const IMPORT_FROM_ROOT_MESSAGE = "Import project files by their p
|
|
|
30
30
|
export declare const MODEL_MEMBERS_MESSAGE = "A model shows the outside world three kinds of public member, so a reader can tell them apart at a glance: getters are its answers (public get hasNext()), command() fields are what it can do (public openNext = command(() => ...)), and onPageOpen() runs when its page opens. Everything else is private, and a private helper that works something out is a method (private neighbours()), never a getter, because Pattern publishes every getter to the screen and the snapshot. A model has no `actions`: menu entries and shortcuts live in src/actions/.";
|
|
31
31
|
export declare const MODEL_BY_NAME_MESSAGE = "Keep the model whole and read it by name: const issueList = useModel('issueList'), then issueList.isLoading and onClick={issueList.createIssue}. Destructuring it hides where each value comes from and leaves loose names a reader has to trace back.";
|
|
32
32
|
export declare const USE_MODEL_WHERE_MESSAGE = "useModel is for screens (src/screens/) and layouts (src/layouts/). Other components take what they show as props and the commands they call as callbacks, so the screen decides what they read: <IssueListRow issue={issue} onDelete={issueList.deleteIssue} />.";
|
|
33
|
-
export declare const PURE_GETTERS_MESSAGE = "A getter works an answer out from this.state and this.stores.<name>.state and nothing else. It never changes anything (this.set, a store method, this.app.navigate), never waits, and never reads the clock, randomness or a private field, because Pattern caches each getter until the state it read changes, and flows compare its value. Put the change in a command.";
|
|
33
|
+
export declare const PURE_GETTERS_MESSAGE = "A getter works an answer out from this.state and this.stores.<name>.state and nothing else. It never changes anything (this.set, a store method, this.app.navigate), never waits, and never reads the clock, randomness or a private field, because Pattern caches each getter until the state it read changes, and flows compare its value. Put the change in a command. For the date today read this.stores.device.state.today (YYYY-MM-DD on this device, kept current past midnight), and for a phone-width screen this.stores.device.state.isCompact.";
|
|
34
34
|
export declare const ACTIONS_WHERE_MESSAGE = "Menu entries and shortcuts are actions, and actions live only in src/actions/: one file per thing they act on, `export const issueActions = defineActions(({ page, models }) => [action({ id: 'issues.next', label: 'Next issue', keys: ['j'], send: [{ type: 'issueDetail.openNext' }] })])`, listed in src/actions/index.ts. If a person can see it in a menu or press a key for it, it is an action; what it does is a command on a model, and an action only sends commands. Models never build actions.";
|
|
35
35
|
export declare const PURE_ACTIONS_MESSAGE = "An actions file works its entries out from `page` and `models` (each model's state and answers) and nothing else, because Pattern reruns it only when something it read changes, and flows compare the result. It never waits, never uses `this`, never reads the clock or randomness, and imports only types from src/models/, src/stores/ and src/components/. If an entry needs something a model does not answer yet, add a getter to that model.";
|
|
36
36
|
export declare const USE_PRIMITIVES_MESSAGE = "Write <Button>, <IconButton>, <TextField>, <Select>, <Switch> or <Dialog> from @trypattern/sdk/react instead of this element. The primitives carry the focus, keyboard, disabled, pending and error behaviour and the design system's look, so a screen only says what the control does. If no primitive fits, the gap is in the SDK; do not build around it here.";
|
package/dist/rules/messages.js
CHANGED
|
@@ -22,7 +22,7 @@ export const DEV_ONLY_SIGN_IN_MESSAGE = 'The server has exactly one dev-only swi
|
|
|
22
22
|
export const STATE_VIA_HOOKS_MESSAGE = "A screen reads only its model: const issueList = useModel('issueList'), then issueList.issues, issueList.isLoading and issueList.createIssue(). Reading .state, .models, .stores or getSnapshot() directly does not subscribe, so the screen would show stale data. If the screen needs something from a store, add a getter for it to the model.";
|
|
23
23
|
export const CLASSES_NOT_FACTORIES_MESSAGE = 'A function that returns an object of methods is a class in disguise: write it as a class, with private fields for its state and methods for its behaviour. One shape for everything that has state keeps the codebase predictable. Plain functions are for pure helpers that compute a value (sortProjects, newClientSideId).';
|
|
24
24
|
export const ACCESS_FLOWS_MESSAGE = "flows/Access.flow.ts runs Pattern's generated access flows, which prove every table's access rules against the seed on each app test. Keep it: `import { accessFlows } from '@trypattern/sdk/flows'` and `accessFlows({ App })`.";
|
|
25
|
-
export const SCHEMA_TABLES_MESSAGE = "schema.ts defines every saved table in one place and says who may do what: `export const tables = defineTables({ issues: { columns: { title: z.string() }, access: { read: 'signedIn', create: 'signedIn', update: 'signedIn', delete: 'signedIn' }, writes: 'optimistic' } })`.
|
|
25
|
+
export const SCHEMA_TABLES_MESSAGE = "schema.ts defines every saved table in one place and says who may do what: `export const tables = defineTables({ issues: { columns: { title: z.string() }, access: { read: 'signedIn', create: 'signedIn', update: 'signedIn', delete: 'signedIn' }, writes: 'optimistic' } })`. When another file needs a table's row type, schema.ts exports it, named for one row (`export type Issue = Row<typeof tables.issues>`). It also exports the emails of the people who run the app, `export const admins: string[] = []`, even when there are none yet, so the question is answered on purpose. Add the line shown.";
|
|
26
26
|
export const NO_UNUSED_EXPORTS_MESSAGE = 'Export only what another file imports. Nothing imports this, so remove `export` (and the code itself if this file does not use it either). Unused exports hide dead code and make a file look like it offers more than it does. Pattern reads `tables` from schema.ts and `seed` from seed.ts, and vite.config.ts and server/worker.ts default-export their config, so those stay.';
|
|
27
27
|
export const NO_RE_EXPORTS_MESSAGE = 'Do not pass imports through: a file exports only what it defines. Import the thing from where it lives instead (command from @trypattern/sdk, a component from its own file), so every import names the real source. server/worker.ts is the one exception: Cloudflare runs the Workflow and Durable Object classes its Worker exports, so it may export them from where they live.';
|
|
28
28
|
export const EXPLICIT_ACCESS_MESSAGE = 'Mark every class member public or private (or protected, to override a base class member), so a reader sees at a glance what other code may call: `public setSort = command(...)`, `public override onPageOpen()`, `private loadIssues()`. Use the word private, never #name, so there is one way to say it. Constructors need neither.';
|
|
@@ -30,7 +30,7 @@ export const IMPORT_FROM_ROOT_MESSAGE = 'Import project files by their path from
|
|
|
30
30
|
export const MODEL_MEMBERS_MESSAGE = 'A model shows the outside world three kinds of public member, so a reader can tell them apart at a glance: getters are its answers (public get hasNext()), command() fields are what it can do (public openNext = command(() => ...)), and onPageOpen() runs when its page opens. Everything else is private, and a private helper that works something out is a method (private neighbours()), never a getter, because Pattern publishes every getter to the screen and the snapshot. A model has no `actions`: menu entries and shortcuts live in src/actions/.';
|
|
31
31
|
export const MODEL_BY_NAME_MESSAGE = "Keep the model whole and read it by name: const issueList = useModel('issueList'), then issueList.isLoading and onClick={issueList.createIssue}. Destructuring it hides where each value comes from and leaves loose names a reader has to trace back.";
|
|
32
32
|
export const USE_MODEL_WHERE_MESSAGE = 'useModel is for screens (src/screens/) and layouts (src/layouts/). Other components take what they show as props and the commands they call as callbacks, so the screen decides what they read: <IssueListRow issue={issue} onDelete={issueList.deleteIssue} />.';
|
|
33
|
-
export const PURE_GETTERS_MESSAGE = 'A getter works an answer out from this.state and this.stores.<name>.state and nothing else. It never changes anything (this.set, a store method, this.app.navigate), never waits, and never reads the clock, randomness or a private field, because Pattern caches each getter until the state it read changes, and flows compare its value. Put the change in a command.';
|
|
33
|
+
export const PURE_GETTERS_MESSAGE = 'A getter works an answer out from this.state and this.stores.<name>.state and nothing else. It never changes anything (this.set, a store method, this.app.navigate), never waits, and never reads the clock, randomness or a private field, because Pattern caches each getter until the state it read changes, and flows compare its value. Put the change in a command. For the date today read this.stores.device.state.today (YYYY-MM-DD on this device, kept current past midnight), and for a phone-width screen this.stores.device.state.isCompact.';
|
|
34
34
|
export const ACTIONS_WHERE_MESSAGE = "Menu entries and shortcuts are actions, and actions live only in src/actions/: one file per thing they act on, `export const issueActions = defineActions(({ page, models }) => [action({ id: 'issues.next', label: 'Next issue', keys: ['j'], send: [{ type: 'issueDetail.openNext' }] })])`, listed in src/actions/index.ts. If a person can see it in a menu or press a key for it, it is an action; what it does is a command on a model, and an action only sends commands. Models never build actions.";
|
|
35
35
|
export const PURE_ACTIONS_MESSAGE = "An actions file works its entries out from `page` and `models` (each model's state and answers) and nothing else, because Pattern reruns it only when something it read changes, and flows compare the result. It never waits, never uses `this`, never reads the clock or randomness, and imports only types from src/models/, src/stores/ and src/components/. If an entry needs something a model does not answer yet, add a getter to that model.";
|
|
36
36
|
export const USE_PRIMITIVES_MESSAGE = "Write <Button>, <IconButton>, <TextField>, <Select>, <Switch> or <Dialog> from @trypattern/sdk/react instead of this element. The primitives carry the focus, keyboard, disabled, pending and error behaviour and the design system's look, so a screen only says what the control does. If no primitive fits, the gap is in the SDK; do not build around it here.";
|
package/dist/server/tables.js
CHANGED
|
@@ -12,7 +12,7 @@ function typesOf(schema) {
|
|
|
12
12
|
return [type];
|
|
13
13
|
return Array.isArray(type) ? type : [];
|
|
14
14
|
}
|
|
15
|
-
function sqlColumn(key, schema) {
|
|
15
|
+
function sqlColumn(key, schema, start) {
|
|
16
16
|
const types = typesOf(schema);
|
|
17
17
|
const name = snakeCase(key);
|
|
18
18
|
const column = types.includes('boolean')
|
|
@@ -22,7 +22,8 @@ function sqlColumn(key, schema) {
|
|
|
22
22
|
: types.includes('number')
|
|
23
23
|
? real(name)
|
|
24
24
|
: text(name);
|
|
25
|
-
|
|
25
|
+
const started = start === undefined ? column : column.default(start);
|
|
26
|
+
return types.includes('null') ? started : started.notNull();
|
|
26
27
|
}
|
|
27
28
|
function uniqueIndexName(table, columns) {
|
|
28
29
|
return [table.name, ...columns, 'unique'].map(snakeCase).join('_');
|
|
@@ -30,7 +31,7 @@ function uniqueIndexName(table, columns) {
|
|
|
30
31
|
export function sqlTable(table) {
|
|
31
32
|
const columns = Object.fromEntries(Object.entries(table.columns).map(([key, schema]) => [
|
|
32
33
|
key,
|
|
33
|
-
sqlColumn(key, schema)
|
|
34
|
+
sqlColumn(key, schema, table.defaults[key])
|
|
34
35
|
]));
|
|
35
36
|
return sqliteTable(snakeCase(table.name), {
|
|
36
37
|
id: text('id').primaryKey(),
|
package/dist/store.d.ts
CHANGED
|
@@ -34,7 +34,8 @@ export declare abstract class DeclaredStore<State extends object, Client = Recor
|
|
|
34
34
|
export type StoreClass<S extends StateSpec, P extends readonly (keyof S & string)[], Client = Record<never, never>> = (abstract new (persisted: Persisted, services: Client) => DeclaredStore<StateOf<S>, Client>) & {
|
|
35
35
|
readonly config: StoreConfig<S, P>;
|
|
36
36
|
};
|
|
37
|
-
export
|
|
37
|
+
export type StoreFactory<Client> = <S extends StateSpec = Record<never, never>, const P extends readonly (keyof S & string)[] = []>(config: StoreConfig<S, P>) => StoreClass<S, P, Client>;
|
|
38
|
+
export declare function defineStore<Client>(): StoreFactory<Client>;
|
|
38
39
|
export type Snapshots<Group> = {
|
|
39
40
|
[Key in keyof Group]: Group[Key] extends {
|
|
40
41
|
readonly state: infer State;
|
package/dist/table.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export type TableDefinition = {
|
|
|
17
17
|
readonly owned?: boolean;
|
|
18
18
|
readonly public?: readonly string[];
|
|
19
19
|
readonly unique?: readonly (readonly string[])[];
|
|
20
|
+
readonly defaults?: Readonly<Record<string, Value>>;
|
|
20
21
|
};
|
|
21
22
|
type OwnerColumn<Owned> = [Owned] extends [true] ? {
|
|
22
23
|
ownerId: z.ZodString;
|
|
@@ -29,6 +30,7 @@ export type Table = {
|
|
|
29
30
|
readonly owned: boolean;
|
|
30
31
|
readonly public: readonly string[];
|
|
31
32
|
readonly unique: readonly (readonly string[])[];
|
|
33
|
+
readonly defaults: Readonly<Record<string, Value>>;
|
|
32
34
|
readonly fields: z.ZodObject<Columns>;
|
|
33
35
|
readonly incoming: z.ZodObject<{
|
|
34
36
|
id: z.ZodString;
|
|
@@ -49,6 +51,7 @@ export type TableOf<Name extends string, Definition extends TableDefinition> = {
|
|
|
49
51
|
readonly owned: Definition['owned'] extends true ? true : false;
|
|
50
52
|
readonly public: readonly PublicColumn<Definition>[];
|
|
51
53
|
readonly unique: readonly (readonly string[])[];
|
|
54
|
+
readonly defaults: Readonly<Record<string, Value>>;
|
|
52
55
|
readonly fields: z.ZodObject<Definition['columns']>;
|
|
53
56
|
readonly incoming: z.ZodObject<{
|
|
54
57
|
id: z.ZodString;
|
|
@@ -96,6 +99,7 @@ type DraftTable = {
|
|
|
96
99
|
readonly owned?: boolean;
|
|
97
100
|
readonly public?: readonly string[];
|
|
98
101
|
readonly unique?: readonly (readonly string[])[];
|
|
102
|
+
readonly defaults?: Readonly<Record<string, Value>>;
|
|
99
103
|
};
|
|
100
104
|
type StaticTypeError<Message extends string> = Message;
|
|
101
105
|
type IsOwned<Definition> = Definition extends {
|
|
@@ -112,6 +116,11 @@ type ReadOf<Definition> = Definition extends {
|
|
|
112
116
|
type ColumnOf<Definition> = Definition extends {
|
|
113
117
|
readonly columns: infer C;
|
|
114
118
|
} ? keyof C & string : never;
|
|
119
|
+
type DefaultsOf<Definition> = Definition extends {
|
|
120
|
+
readonly columns: infer C extends Columns;
|
|
121
|
+
} ? {
|
|
122
|
+
readonly [Key in keyof C]?: z.output<C[Key]>;
|
|
123
|
+
} : never;
|
|
115
124
|
type OwnerNeedsOwned = StaticTypeError<"'owner' needs owned: true on this table, so every row records who made it">;
|
|
116
125
|
type CreateIsNeverOwner = StaticTypeError<"create can't be 'owner': nobody owns a row before it exists. Use 'signedIn' and the person who creates it becomes its owner">;
|
|
117
126
|
type OwnedNeedsSignedInCreate = StaticTypeError<"An owned table can't let anyone create rows: a signed-out visitor has no account to own the row. Use 'signedIn'">;
|
|
@@ -132,7 +141,7 @@ type CheckedColumns<C> = {
|
|
|
132
141
|
[Key in keyof C]: Key extends 'ownerId' ? OwnerIdIsPattern : C[Key];
|
|
133
142
|
};
|
|
134
143
|
type CheckedTable<Definition> = {
|
|
135
|
-
[Key in keyof Definition]: Key extends 'columns' ? CheckedColumns<SameOnTheWire<Definition[Key] & z.ZodRawShape>> : Key extends 'access' ? CheckedAccess<Definition, Definition[Key]> : Key extends 'writes' ? HasUnique<Definition> extends true ? Definition[Key] extends 'confirmed' ? Definition[Key] : UniqueNeedsConfirmed : Definition[Key] : Key extends 'public' ? ReadOf<Definition> extends 'anyone' ? PublicNeedsPrivateRead : Definition[Key] & readonly (ColumnOf<Definition> | 'createdAt')[] : Key extends 'unique' ? Definition[Key] & readonly (readonly ColumnOf<Definition>[])[] : Definition[Key];
|
|
144
|
+
[Key in keyof Definition]: Key extends 'columns' ? CheckedColumns<SameOnTheWire<Definition[Key] & z.ZodRawShape>> : Key extends 'access' ? CheckedAccess<Definition, Definition[Key]> : Key extends 'writes' ? HasUnique<Definition> extends true ? Definition[Key] extends 'confirmed' ? Definition[Key] : UniqueNeedsConfirmed : Definition[Key] : Key extends 'public' ? ReadOf<Definition> extends 'anyone' ? PublicNeedsPrivateRead : Definition[Key] & readonly (ColumnOf<Definition> | 'createdAt')[] : Key extends 'unique' ? Definition[Key] & readonly (readonly ColumnOf<Definition>[])[] : Key extends 'defaults' ? Definition[Key] & DefaultsOf<Definition> : Definition[Key];
|
|
136
145
|
} & Missing<Definition, 'access', AccessIsRequired> & Missing<Definition, 'writes', WritesIsRequired>;
|
|
137
146
|
export declare function defineTables<const Definitions extends Record<string, DraftTable> = Record<never, never>>(definitions: {
|
|
138
147
|
[Key in keyof Definitions]: CheckedTable<Definitions[Key]>;
|
package/dist/table.js
CHANGED
|
@@ -28,6 +28,15 @@ function problemIn(name, definition) {
|
|
|
28
28
|
if (unknown !== undefined) {
|
|
29
29
|
return `Table "${name}" names "${unknown}", which is not one of its columns`;
|
|
30
30
|
}
|
|
31
|
+
for (const [column, value] of Object.entries(definition.defaults ?? {})) {
|
|
32
|
+
const schema = columns[column];
|
|
33
|
+
if (schema === undefined) {
|
|
34
|
+
return `Table "${name}" has a default for "${column}", which is not one of its columns`;
|
|
35
|
+
}
|
|
36
|
+
if (!schema.safeParse(value).success) {
|
|
37
|
+
return `Table "${name}": the default for "${column}" isn't a valid ${column}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
31
40
|
if ((definition.unique ?? []).length > 0 &&
|
|
32
41
|
definition.writes !== 'confirmed') {
|
|
33
42
|
return `Table "${name}" has a unique constraint, so writes must be 'confirmed'`;
|
|
@@ -65,6 +74,7 @@ function table(name, definition, isPatterns = false) {
|
|
|
65
74
|
owned,
|
|
66
75
|
public: publicColumns,
|
|
67
76
|
unique: definition.unique ?? [],
|
|
77
|
+
defaults: definition.defaults ?? {},
|
|
68
78
|
fields: z.object(columns),
|
|
69
79
|
incoming: z.object({ id: z.string(), ...columns }),
|
|
70
80
|
row: z.object({
|