@zeno-lib/schema 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +120 -14
- package/dist/index.mjs +137 -28
- package/package.json +15 -15
- package/src/index.test-d.ts +28 -38
- package/src/index.test.ts +144 -71
- package/src/index.ts +504 -91
- package/src/test-table.ts +11 -0
package/dist/index.d.mts
CHANGED
|
@@ -7,28 +7,134 @@ type NoTableSchemaOptions = {
|
|
|
7
7
|
[noTableSchemaOptions]?: never;
|
|
8
8
|
};
|
|
9
9
|
type TableColumns<TTable extends Table> = TTable["_"]["columns"];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
declare const COLUMN_RULES_KEY = "~zeno";
|
|
11
|
+
/**
|
|
12
|
+
* What a column marker from `@zeno-lib/db/schema` records. `writer` says who
|
|
13
|
+
* writes the column when a client doesn't: `"server"` code (`serverOwned`), or
|
|
14
|
+
* only the `"database"`, through a default and a trigger (the audit columns
|
|
15
|
+
* `timestamps()` and `authorship()` emit). `refine` runs on it in insert and
|
|
16
|
+
* update, and `hint` goes to the form field that edits it.
|
|
17
|
+
*/
|
|
18
|
+
type ColumnRule = {
|
|
19
|
+
readonly writer?: "database" | "server";
|
|
20
|
+
readonly refine?: unknown;
|
|
21
|
+
readonly hint?: FieldHint;
|
|
22
|
+
};
|
|
23
|
+
type Writer = NonNullable<ColumnRule["writer"]>;
|
|
24
|
+
/**
|
|
25
|
+
* A table built by `table()`, `unsecureTable()` or `schema(...).table` from
|
|
26
|
+
* `@zeno-lib/db/schema`, which records its column rules. A plain drizzle
|
|
27
|
+
* table has none, so its markers would vanish without a trace.
|
|
28
|
+
*/
|
|
29
|
+
type ZenoTable = Table & {
|
|
30
|
+
readonly [COLUMN_RULES_KEY]: {
|
|
31
|
+
readonly columns: object;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
type TableColumnRules<TTable extends ZenoTable> = TTable[typeof COLUMN_RULES_KEY]["columns"];
|
|
35
|
+
type WrittenByKeys<TTable extends ZenoTable, TWriter extends Writer> = { [K in keyof TableColumnRules<TTable>]: TableColumnRules<TTable>[K] extends {
|
|
36
|
+
readonly writer: TWriter;
|
|
37
|
+
} ? K : never; }[keyof TableColumnRules<TTable>];
|
|
38
|
+
type ColumnRefinements<TTable extends ZenoTable> = { [K in keyof TableColumnRules<TTable> as TableColumnRules<TTable>[K] extends {
|
|
39
|
+
readonly refine: unknown;
|
|
40
|
+
} ? K : never]: TableColumnRules<TTable>[K] extends {
|
|
41
|
+
readonly refine: infer TRefine;
|
|
42
|
+
} ? TRefine : never; };
|
|
43
|
+
type IdentityKeys<TTable extends Table> = { [K in keyof TableColumns<TTable>]: TableColumns<TTable>[K]["_"]["identity"] extends "always" | "byDefault" ? K : never; }[keyof TableColumns<TTable>];
|
|
44
|
+
type ClientExcludes = Writer;
|
|
45
|
+
type ServerExcludes = "database";
|
|
46
|
+
type UnwritableKeys<TTable extends ZenoTable, TExcludes extends Writer = ClientExcludes> = IdentityKeys<TTable> | WrittenByKeys<TTable, TExcludes>;
|
|
47
|
+
type InsertColumns<TTable extends ZenoTable, TExcludes extends Writer = ClientExcludes> = Pick<TableColumns<TTable>, Exclude<keyof InferInsertModel<TTable>, UnwritableKeys<TTable, TExcludes>>>;
|
|
13
48
|
type SelectRefine<TTable extends Table, TCoerce extends CoerceOptions> = BuildRefine<TableColumns<TTable>, TCoerce>;
|
|
14
|
-
type
|
|
15
|
-
type
|
|
16
|
-
type DefineTableSchemaOptions<TTable extends Table, TCoerce extends CoerceOptions = undefined> = {
|
|
49
|
+
type WriteRefineOptions<TTable extends ZenoTable, TCoerce extends CoerceOptions, TExcludes extends Writer> = BuildRefine<InsertColumns<TTable, TExcludes>, TCoerce>;
|
|
50
|
+
type DefineTableSchemaOptions<TTable extends ZenoTable, TCoerce extends CoerceOptions = undefined, TExcludes extends Writer = ClientExcludes> = {
|
|
17
51
|
factory?: CreateSchemaFactoryOptions<TCoerce>;
|
|
18
|
-
insert?: NoUnknownKeys<
|
|
52
|
+
insert?: NoUnknownKeys<WriteRefineOptions<TTable, TCoerce, TExcludes>, InferInsertModel<TTable>>;
|
|
19
53
|
select?: NoUnknownKeys<SelectRefine<TTable, TCoerce>, InferSelectModel<TTable>>;
|
|
20
|
-
update?:
|
|
54
|
+
update?: WriteRefineOptions<TTable, TCoerce, TExcludes>;
|
|
21
55
|
};
|
|
22
56
|
type RefineFromOptions<TOptions, TKey extends string> = TKey extends keyof TOptions ? NonNullable<TOptions[TKey]> extends Record<string, unknown> ? NonNullable<TOptions[TKey]> : undefined : undefined;
|
|
57
|
+
type WriteRefine<TTable extends ZenoTable, TOptions, TKey extends string> = RefineFromOptions<TOptions, TKey> extends (infer TCaller) ? TCaller extends Record<string, unknown> ? Omit<ColumnRefinements<TTable>, keyof TCaller> & TCaller : ColumnRefinements<TTable> : never;
|
|
23
58
|
type OmitKeys<TSchema, TKeys extends PropertyKey> = TSchema extends z.ZodObject<infer TShape, infer TConfig> ? z.ZodObject<{ [K in keyof TShape as K extends TKeys ? never : K]: TShape[K]; }, TConfig> : never;
|
|
24
59
|
type IsInsertOptional<TColumn> = TColumn extends Column ? TColumn["_"]["notNull"] extends true ? TColumn["_"]["hasDefault"] extends true ? true : false : true : false;
|
|
25
60
|
type OptionalRefinedInsert<TSchema, TTable extends Table, TRefine> = TSchema extends z.ZodObject<infer TShape, infer TConfig> ? z.ZodObject<{ [K in keyof TShape]: K extends keyof TRefine ? TRefine[K] extends ((schema: never) => unknown) ? (K extends keyof TableColumns<TTable> ? IsInsertOptional<TableColumns<TTable>[K]> : false) extends true ? TShape[K] extends z.ZodOptional ? TShape[K] : z.ZodOptional<TShape[K]> : TShape[K] : TShape[K] : TShape[K]; }, TConfig> : never;
|
|
26
|
-
type DefineTableSchemaResult<TTable extends
|
|
27
|
-
|
|
61
|
+
type DefineTableSchemaResult<TTable extends ZenoTable, TCoerce extends CoerceOptions, TOptions, TExcludes extends Writer = ClientExcludes> = {
|
|
62
|
+
/** The table the schemas come from, for `ownedRows`. */
|
|
63
|
+
table: TTable;
|
|
64
|
+
insert: OmitKeys<OptionalRefinedInsert<BuildSchema<"insert", TableColumns<TTable>, WriteRefine<TTable, TOptions, "insert">, TCoerce>, TTable, WriteRefine<TTable, TOptions, "insert">>, UnwritableKeys<TTable, TExcludes>>;
|
|
28
65
|
select: BuildSchema<"select", TableColumns<TTable>, RefineFromOptions<TOptions, "select">, TCoerce>;
|
|
29
|
-
update: OmitKeys<BuildSchema<"update", TableColumns<TTable>,
|
|
66
|
+
update: OmitKeys<BuildSchema<"update", TableColumns<TTable>, WriteRefine<TTable, TOptions, "update">, TCoerce>, UnwritableKeys<TTable, TExcludes>>;
|
|
67
|
+
};
|
|
68
|
+
/** What a column's `withRule` hints the form field that edits it. */
|
|
69
|
+
type FieldHint = {
|
|
70
|
+
readonly field: string;
|
|
71
|
+
};
|
|
72
|
+
declare function defineTableSchema<TTable extends ZenoTable>(table: TTable): DefineTableSchemaResult<TTable, undefined, NoTableSchemaOptions>;
|
|
73
|
+
declare function defineTableSchema<TTable extends ZenoTable, TCoerce extends CoerceOptions = undefined, TOptions extends DefineTableSchemaOptions<TTable, TCoerce> = DefineTableSchemaOptions<TTable, TCoerce>>(table: TTable, options: TOptions): DefineTableSchemaResult<TTable, TCoerce, TOptions>;
|
|
74
|
+
/**
|
|
75
|
+
* `defineTableSchema` for code that runs only on the server, such as a sync
|
|
76
|
+
* job, which writes the columns `serverOwned()` keeps from clients. Identity
|
|
77
|
+
* and audit columns stay out, as the database writes them. Never parse what a
|
|
78
|
+
* client sends with it: a server action's input is a client's.
|
|
79
|
+
*/
|
|
80
|
+
declare function defineServerTableSchema<TTable extends ZenoTable>(table: TTable): DefineTableSchemaResult<TTable, undefined, NoTableSchemaOptions, ServerExcludes>;
|
|
81
|
+
declare function defineServerTableSchema<TTable extends ZenoTable, TCoerce extends CoerceOptions = undefined, TOptions extends DefineTableSchemaOptions<TTable, TCoerce, ServerExcludes> = DefineTableSchemaOptions<TTable, TCoerce, ServerExcludes>>(table: TTable, options: TOptions): DefineTableSchemaResult<TTable, TCoerce, TOptions, ServerExcludes>;
|
|
82
|
+
type ColumnKey<TTable extends Table> = Extract<keyof TableColumns<TTable>, string>;
|
|
83
|
+
type OwnableSchema = {
|
|
84
|
+
insert: z.ZodObject;
|
|
85
|
+
select: {
|
|
86
|
+
shape: {
|
|
87
|
+
id: z.ZodType;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
table: ZenoTable;
|
|
91
|
+
};
|
|
92
|
+
type OwnedRowSchema<TSchema extends OwnableSchema, TParentKey extends string> = TSchema["insert"] extends z.ZodObject<infer TShape, infer TConfig> ? z.ZodObject<{ [K in keyof TShape as K extends TParentKey ? never : K]: TShape[K]; } & {
|
|
93
|
+
id: z.ZodOptional<TSchema["select"]["shape"]["id"]>;
|
|
94
|
+
}, TConfig> : never;
|
|
95
|
+
/**
|
|
96
|
+
* A table's rows that belong to one parent row and are edited as a list, such
|
|
97
|
+
* as an order's lines. `syncRows` from `@zeno-lib/db/query` takes it in place
|
|
98
|
+
* of the table, and a form validates each row with `row`.
|
|
99
|
+
*/
|
|
100
|
+
type OwnedRows<TTable extends ZenoTable, TParentKey extends string, TRow extends z.ZodObject> = {
|
|
101
|
+
/** The columns that point at the parent row, which `row` leaves out. */
|
|
102
|
+
readonly parent: readonly TParentKey[];
|
|
103
|
+
/**
|
|
104
|
+
* One row as a client sends it: the `insert` columns but the parent's, and
|
|
105
|
+
* the `id` of a stored row, absent on a new one.
|
|
106
|
+
*/
|
|
107
|
+
readonly row: TRow;
|
|
108
|
+
readonly table: TTable;
|
|
30
109
|
};
|
|
31
|
-
|
|
32
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The rows of `schema`'s table that belong to a parent through the `parent`
|
|
112
|
+
* columns, for a form that edits them as a list and the `syncRows` that saves
|
|
113
|
+
* it:
|
|
114
|
+
*
|
|
115
|
+
* ```ts
|
|
116
|
+
* const orderLines = ownedRows(defineTableSchema(lines), { parent: "orderId" })
|
|
117
|
+
*
|
|
118
|
+
* const orderSchema = defineTableSchema(orders).insert.extend({
|
|
119
|
+
* lines: z.array(orderLines.row),
|
|
120
|
+
* })
|
|
121
|
+
*
|
|
122
|
+
* await syncRows(tx, orderLines, { parent: { orderId }, rows: input.lines })
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* The table needs a sequential `id`. A link table matched by a natural key
|
|
126
|
+
* goes to `syncRows` with its `key` option instead.
|
|
127
|
+
*/
|
|
128
|
+
declare const ownedRows: <TSchema extends OwnableSchema, TParentKey extends ColumnKey<TSchema["table"]>>(schema: TSchema, { parent }: {
|
|
129
|
+
parent: TParentKey | readonly TParentKey[];
|
|
130
|
+
}) => OwnedRows<TSchema["table"], TParentKey, OwnedRowSchema<TSchema, TParentKey>>;
|
|
131
|
+
/**
|
|
132
|
+
* The columns a client may write, for every Zeno table among `exports` (a
|
|
133
|
+
* schema barrel, say), keyed by name (`schema.table` outside `public`): what `defineTableSchema`'s
|
|
134
|
+
* `insert` accepts, and `update` accepts the same. Columns are writable unless
|
|
135
|
+
* marked, so snapshot this in one test, and a newly writable column shows up
|
|
136
|
+
* as a diff in review.
|
|
137
|
+
*/
|
|
138
|
+
declare const writableColumns: (exports: Record<string, unknown>) => Record<string, string[]>;
|
|
33
139
|
//#endregion
|
|
34
|
-
export { type DefineTableSchemaOptions, type DefineTableSchemaResult, createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema, defineTableSchema };
|
|
140
|
+
export { type DefineTableSchemaOptions, type DefineTableSchemaResult, type OwnedRows, type ZenoTable, createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema, defineServerTableSchema, defineTableSchema, ownedRows, writableColumns };
|
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,52 @@
|
|
|
1
|
-
import { getColumns } from "drizzle-orm";
|
|
1
|
+
import { Table, getColumns, getTableName, getTableUniqueName, is } from "drizzle-orm";
|
|
2
2
|
import { createInsertSchema, createSchemaFactory, createSchemaFactory as createSchemaFactory$1, createSelectSchema, createUpdateSchema } from "drizzle-orm/zod";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
//#region src/index.ts
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
]);
|
|
10
|
-
const auditKeys = (table) => {
|
|
5
|
+
const COLUMN_RULES_KEY = "~zeno";
|
|
6
|
+
const columnRules = (table) => table[COLUMN_RULES_KEY].columns;
|
|
7
|
+
const CLIENT_EXCLUDES = /* @__PURE__ */ new Set(["database", "server"]);
|
|
8
|
+
const SERVER_EXCLUDES = /* @__PURE__ */ new Set(["database"]);
|
|
9
|
+
const unwritableKeys = (table, excludes) => {
|
|
11
10
|
const columns = getColumns(table);
|
|
11
|
+
const rules = columnRules(table);
|
|
12
12
|
const keys = [];
|
|
13
|
-
for (const key of Object.keys(columns))
|
|
13
|
+
for (const key of Object.keys(columns)) {
|
|
14
|
+
const isIdentity = columns[key]?.generatedIdentity !== void 0;
|
|
15
|
+
const writer = rules[key]?.writer;
|
|
16
|
+
if (isIdentity || writer !== void 0 && excludes.has(writer)) keys.push(key);
|
|
17
|
+
}
|
|
14
18
|
return keys;
|
|
15
19
|
};
|
|
20
|
+
const composeRefinements = (refinements, generated) => {
|
|
21
|
+
const merged = { ...refinements };
|
|
22
|
+
for (const key of Object.keys(generated)) {
|
|
23
|
+
const inner = generated[key];
|
|
24
|
+
const refinement = refinements?.[key];
|
|
25
|
+
if (refinement === void 0) merged[key] = inner;
|
|
26
|
+
else if (typeof refinement === "function") merged[key] = (schema) => refinement(inner(schema));
|
|
27
|
+
}
|
|
28
|
+
return merged;
|
|
29
|
+
};
|
|
30
|
+
const FIELD_HINTS_KEY = Symbol.for("@zeno-lib/field-hints");
|
|
31
|
+
const fieldHints = (() => {
|
|
32
|
+
const holder = globalThis;
|
|
33
|
+
holder[FIELD_HINTS_KEY] ??= z.registry();
|
|
34
|
+
return holder[FIELD_HINTS_KEY];
|
|
35
|
+
})();
|
|
36
|
+
const withHint = (rule, hint) => (schema) => {
|
|
37
|
+
const ruled = rule(schema);
|
|
38
|
+
fieldHints.add(ruled, hint);
|
|
39
|
+
return ruled;
|
|
40
|
+
};
|
|
41
|
+
const withRuleRefinements = (table, refinements) => {
|
|
42
|
+
const rules = columnRules(table);
|
|
43
|
+
const generated = {};
|
|
44
|
+
for (const key of Object.keys(rules)) {
|
|
45
|
+
const { hint, refine } = rules[key] ?? {};
|
|
46
|
+
if (typeof refine === "function") generated[key] = hint === void 0 ? refine : withHint(refine, hint);
|
|
47
|
+
}
|
|
48
|
+
return composeRefinements(refinements, generated);
|
|
49
|
+
};
|
|
16
50
|
const isPositiveIdentity = (column) => {
|
|
17
51
|
const identity = column.generatedIdentity;
|
|
18
52
|
if (identity === void 0) return false;
|
|
@@ -20,33 +54,108 @@ const isPositiveIdentity = (column) => {
|
|
|
20
54
|
return Number(increment) > 0 && Number(minValue) > 0 && Number(startWith) > 0;
|
|
21
55
|
};
|
|
22
56
|
const toPositive = (schema) => typeof schema.positive === "function" ? schema.positive() : schema;
|
|
23
|
-
const
|
|
57
|
+
const positiveCheck = (column) => isPositiveIdentity(column) ? toPositive : void 0;
|
|
58
|
+
const TIMESTAMP = /^(\d{4}-\d{2}-\d{2})[T ](?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?([+-](?:0\d|1[0-5])(?::?[0-5]\d)?|Z)?$/;
|
|
59
|
+
const INFINITIES = /* @__PURE__ */ new Set(["-infinity", "infinity"]);
|
|
60
|
+
const ISO_DATE = z.iso.date();
|
|
61
|
+
const isDate = (value) => ISO_DATE.safeParse(value).success;
|
|
62
|
+
const isTimestamp = (withTimezone) => (value) => {
|
|
63
|
+
const [, day, offset] = TIMESTAMP.exec(value) ?? [];
|
|
64
|
+
return day !== void 0 && isDate(day) && offset !== void 0 === withTimezone;
|
|
65
|
+
};
|
|
66
|
+
const withFormat = (format, isValid) => (schema) => schema.check((context) => {
|
|
67
|
+
if (!(INFINITIES.has(context.value) || isValid(context.value))) context.issues.push({
|
|
68
|
+
code: "invalid_format",
|
|
69
|
+
continue: true,
|
|
70
|
+
format,
|
|
71
|
+
input: context.value
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
const formatCheck = (column) => {
|
|
75
|
+
const { dimensions = 0, withTimezone = false } = column;
|
|
76
|
+
if (dimensions > 0) return;
|
|
77
|
+
switch (column.dataType) {
|
|
78
|
+
case "string date": return withFormat("date", isDate);
|
|
79
|
+
case "string timestamp": return withFormat("datetime", isTimestamp(withTimezone));
|
|
80
|
+
default: return;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const withChecks = (table, checkFor, refinements) => {
|
|
24
84
|
const columns = getColumns(table);
|
|
25
|
-
const
|
|
85
|
+
const generated = {};
|
|
26
86
|
for (const key of Object.keys(columns)) {
|
|
27
87
|
const column = columns[key];
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (refinement === void 0) merged[key] = toPositive;
|
|
31
|
-
else if (typeof refinement === "function") merged[key] = (schema) => refinement(toPositive(schema));
|
|
88
|
+
const check = column === void 0 ? void 0 : checkFor(column);
|
|
89
|
+
if (check !== void 0) generated[key] = check;
|
|
32
90
|
}
|
|
33
|
-
return
|
|
91
|
+
return composeRefinements(refinements, generated);
|
|
34
92
|
};
|
|
35
|
-
|
|
93
|
+
const omitPresent = (schema, keys) => {
|
|
94
|
+
const mask = {};
|
|
95
|
+
for (const key of keys) if (key in schema.shape) mask[key] = true;
|
|
96
|
+
return schema.omit(mask);
|
|
97
|
+
};
|
|
98
|
+
const buildTableSchema = (name, table, options, excludes) => {
|
|
99
|
+
if (!(COLUMN_RULES_KEY in table)) throw new TypeError(`${name} reads the column rules of a table from table(), unsecureTable() or schema(...).table in @zeno-lib/db/schema, and "${getTableName(table)}" has none.`);
|
|
36
100
|
const schemaFactory = createSchemaFactory$1(options?.factory);
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const mask = {};
|
|
40
|
-
for (const key of audit) if (key in schema.shape) mask[key] = true;
|
|
41
|
-
return schema.omit(mask);
|
|
42
|
-
};
|
|
101
|
+
const unwritable = unwritableKeys(table, excludes);
|
|
102
|
+
const omitUnwritable = (schema) => omitPresent(schema, unwritable);
|
|
43
103
|
const factory = schemaFactory;
|
|
44
|
-
const
|
|
104
|
+
const write = (variant, create) => omitUnwritable(create(table, withChecks(table, formatCheck, withRuleRefinements(table, options?.[variant]))));
|
|
45
105
|
return {
|
|
46
|
-
insert:
|
|
47
|
-
select:
|
|
48
|
-
|
|
106
|
+
insert: write("insert", factory.createInsertSchema),
|
|
107
|
+
select: factory.createSelectSchema(table, withChecks(table, positiveCheck, options?.select)),
|
|
108
|
+
table,
|
|
109
|
+
update: write("update", factory.createUpdateSchema)
|
|
49
110
|
};
|
|
111
|
+
};
|
|
112
|
+
function defineTableSchema(table, options) {
|
|
113
|
+
return buildTableSchema("defineTableSchema", table, options, CLIENT_EXCLUDES);
|
|
114
|
+
}
|
|
115
|
+
function defineServerTableSchema(table, options) {
|
|
116
|
+
return buildTableSchema("defineServerTableSchema", table, options, SERVER_EXCLUDES);
|
|
50
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The rows of `schema`'s table that belong to a parent through the `parent`
|
|
120
|
+
* columns, for a form that edits them as a list and the `syncRows` that saves
|
|
121
|
+
* it:
|
|
122
|
+
*
|
|
123
|
+
* ```ts
|
|
124
|
+
* const orderLines = ownedRows(defineTableSchema(lines), { parent: "orderId" })
|
|
125
|
+
*
|
|
126
|
+
* const orderSchema = defineTableSchema(orders).insert.extend({
|
|
127
|
+
* lines: z.array(orderLines.row),
|
|
128
|
+
* })
|
|
129
|
+
*
|
|
130
|
+
* await syncRows(tx, orderLines, { parent: { orderId }, rows: input.lines })
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* The table needs a sequential `id`. A link table matched by a natural key
|
|
134
|
+
* goes to `syncRows` with its `key` option instead.
|
|
135
|
+
*/
|
|
136
|
+
const ownedRows = (schema, { parent }) => {
|
|
137
|
+
const parentKeys = typeof parent === "string" ? [parent] : [...parent];
|
|
138
|
+
const id = schema.select.shape.id;
|
|
139
|
+
if (id === void 0) throw new TypeError(`ownedRows tells rows apart by their id, and "${getTableName(schema.table)}" has none.`);
|
|
140
|
+
return {
|
|
141
|
+
parent: parentKeys,
|
|
142
|
+
row: omitPresent(schema.insert, parentKeys).extend({ id: id.optional() }),
|
|
143
|
+
table: schema.table
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
const isZenoTable = (value) => is(value, Table) && COLUMN_RULES_KEY in value;
|
|
147
|
+
/**
|
|
148
|
+
* The columns a client may write, for every Zeno table among `exports` (a
|
|
149
|
+
* schema barrel, say), keyed by name (`schema.table` outside `public`): what `defineTableSchema`'s
|
|
150
|
+
* `insert` accepts, and `update` accepts the same. Columns are writable unless
|
|
151
|
+
* marked, so snapshot this in one test, and a newly writable column shows up
|
|
152
|
+
* as a diff in review.
|
|
153
|
+
*/
|
|
154
|
+
const writableColumns = (exports) => {
|
|
155
|
+
const tables = Object.keys(exports).map((key) => exports[key]).filter(isZenoTable).map((table) => [getTableUniqueName(table), table]).sort(([a], [b]) => a < b ? -1 : 1);
|
|
156
|
+
const columns = {};
|
|
157
|
+
for (const [name, table] of tables) columns[name] = Object.keys(buildTableSchema("writableColumns", table, void 0, CLIENT_EXCLUDES).insert.shape).sort();
|
|
158
|
+
return columns;
|
|
159
|
+
};
|
|
51
160
|
//#endregion
|
|
52
|
-
export { createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema, defineTableSchema };
|
|
161
|
+
export { createInsertSchema, createSchemaFactory, createSelectSchema, createUpdateSchema, defineServerTableSchema, defineTableSchema, ownedRows, writableColumns };
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeno-lib/schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"url": "https://github.com/zeno-lib/zeno.git",
|
|
6
|
+
"directory": "packages/schema"
|
|
7
|
+
},
|
|
4
8
|
"type": "module",
|
|
5
9
|
"exports": {
|
|
6
10
|
".": {
|
|
@@ -13,24 +17,20 @@
|
|
|
13
17
|
"dist",
|
|
14
18
|
"src"
|
|
15
19
|
],
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@zeno-lib/typescript": "^1.2.0",
|
|
22
|
+
"drizzle-orm": "1.0.0-rc.5-5935859",
|
|
23
|
+
"tsdown": "^0.23.0",
|
|
24
|
+
"typescript": "7.0.2",
|
|
25
|
+
"vitest": "5.0.3",
|
|
26
|
+
"zod": "^4.6.5"
|
|
22
27
|
},
|
|
23
28
|
"peerDependencies": {
|
|
24
|
-
"drizzle-orm": "1.0.0-rc.
|
|
29
|
+
"drizzle-orm": "1.0.0-rc.5-5935859",
|
|
25
30
|
"zod": ">=4"
|
|
26
31
|
},
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"tsdown": "^0.22.14",
|
|
30
|
-
"typescript": "7.0.2",
|
|
31
|
-
"vitest": "4.1.11",
|
|
32
|
-
"zod": "^4.4.3",
|
|
33
|
-
"@zeno-lib/typescript": "^1.1.0"
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsdown",
|
package/src/index.test-d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
|
|
1
|
+
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"
|
|
3
2
|
import { expectTypeOf, test } from "vitest"
|
|
4
3
|
import { z } from "zod"
|
|
5
4
|
|
|
6
5
|
import { defineTableSchema } from "./index"
|
|
6
|
+
import { zenoTable } from "./test-table"
|
|
7
7
|
|
|
8
8
|
type StandardSchema<T> = {
|
|
9
9
|
readonly "~standard": {
|
|
@@ -12,17 +12,17 @@ type StandardSchema<T> = {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function acceptsFormsSchema<TFormData>(
|
|
15
|
+
declare function acceptsFormsSchema<TFormData>(
|
|
16
16
|
_schema: StandardSchema<TFormData>
|
|
17
|
-
): void
|
|
18
|
-
return
|
|
19
|
-
}
|
|
17
|
+
): void
|
|
20
18
|
|
|
21
|
-
const posts =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
const posts = zenoTable(
|
|
20
|
+
pgTable("posts", {
|
|
21
|
+
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
|
22
|
+
slug: text("slug").notNull(),
|
|
23
|
+
title: text("title").notNull(),
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
26
|
|
|
27
27
|
test("insert and update schemas infer form-compatible Standard Schema data", () => {
|
|
28
28
|
const schemas = defineTableSchema(posts)
|
|
@@ -54,39 +54,29 @@ test("refined schemas preserve refinement-specific output types", () => {
|
|
|
54
54
|
}>()
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
test("
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const schemas = defineTableSchema(
|
|
57
|
+
test("a column named like an audit column stays in the insert type", () => {
|
|
58
|
+
const events = zenoTable(
|
|
59
|
+
pgTable("events", {
|
|
60
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
61
|
+
label: text("label").notNull(),
|
|
62
|
+
})
|
|
63
|
+
)
|
|
64
|
+
const schemas = defineTableSchema(events)
|
|
65
65
|
|
|
66
66
|
expectTypeOf<z.infer<typeof schemas.insert>>().toEqualTypeOf<{
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
createdAt?: Date | undefined
|
|
68
|
+
label: string
|
|
69
69
|
}>()
|
|
70
|
-
expectTypeOf<z.infer<typeof schemas.update>>().toEqualTypeOf<{
|
|
71
|
-
body?: string | undefined
|
|
72
|
-
updatedAt?: Date | undefined
|
|
73
|
-
}>()
|
|
74
|
-
expectTypeOf<keyof z.infer<typeof schemas.select>>().toEqualTypeOf<
|
|
75
|
-
"body" | "createdAt" | "createdBy" | "updatedAt"
|
|
76
|
-
>()
|
|
77
|
-
|
|
78
|
-
defineTableSchema(notes, {
|
|
79
|
-
// @ts-expect-error: an omitted audit column has nothing to refine.
|
|
80
|
-
insert: { createdBy: (schema) => schema },
|
|
81
|
-
})
|
|
82
70
|
})
|
|
83
71
|
|
|
84
72
|
test("a function refinement keeps a nullable or defaulted insert column optional", () => {
|
|
85
|
-
const drafts =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
73
|
+
const drafts = zenoTable(
|
|
74
|
+
pgTable("drafts", {
|
|
75
|
+
note: text("note"),
|
|
76
|
+
status: text("status").notNull().default("open"),
|
|
77
|
+
title: text("title").notNull(),
|
|
78
|
+
})
|
|
79
|
+
)
|
|
90
80
|
const schemas = defineTableSchema(drafts, {
|
|
91
81
|
insert: {
|
|
92
82
|
note: (schema) => schema.min(1),
|