cogsbox-shape 0.5.186 → 0.5.188
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/cogsbox-shape-db/dist/connect.d.ts +73 -0
- package/cogsbox-shape-db/dist/connect.js +146 -0
- package/cogsbox-shape-db/dist/errors.d.ts +7 -0
- package/cogsbox-shape-db/dist/errors.js +14 -0
- package/cogsbox-shape-db/dist/index.d.ts +4 -0
- package/cogsbox-shape-db/dist/index.js +3 -0
- package/cogsbox-shape-db/dist/remap.d.ts +5 -0
- package/cogsbox-shape-db/dist/remap.js +22 -0
- package/cogsbox-shape-db/dist/result-mapper.d.ts +7 -0
- package/cogsbox-shape-db/dist/result-mapper.js +64 -0
- package/cogsbox-shape-db/dist/sql-builder.d.ts +68 -0
- package/cogsbox-shape-db/dist/sql-builder.js +94 -0
- package/cogsbox-shape-db/dist/sqlite/index.d.ts +1 -0
- package/cogsbox-shape-db/dist/sqlite/index.js +1 -0
- package/cogsbox-shape-db/dist/sqlite/sqlite-driver.d.ts +2 -0
- package/cogsbox-shape-db/dist/sqlite/sqlite-driver.js +16 -0
- package/cogsbox-shape-db/dist/table-db.d.ts +50 -0
- package/cogsbox-shape-db/dist/table-db.js +307 -0
- package/cogsbox-shape-db/dist/types.d.ts +39 -0
- package/cogsbox-shape-db/dist/types.js +1 -0
- package/cogsbox-shape-db/dist/where-builder.d.ts +4 -0
- package/cogsbox-shape-db/dist/where-builder.js +71 -0
- package/dist/schema.d.ts +15 -1
- package/dist/schema.js +92 -2
- package/package.json +27 -5
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { Kysely, sql } from "kysely";
|
|
2
|
+
import { buildWhereConditions, buildPkConditions } from "./where-builder.js";
|
|
3
|
+
import { RecordNotFoundError } from "./errors.js";
|
|
4
|
+
export class TableDB {
|
|
5
|
+
db;
|
|
6
|
+
meta;
|
|
7
|
+
transforms;
|
|
8
|
+
reconcile;
|
|
9
|
+
constructor(db, meta, transforms, reconcile) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.meta = meta;
|
|
12
|
+
this.transforms = transforms;
|
|
13
|
+
this.reconcile = reconcile;
|
|
14
|
+
}
|
|
15
|
+
async findMany(opts) {
|
|
16
|
+
const qb = this.db;
|
|
17
|
+
let query = qb.selectFrom(this.meta.tableName).selectAll();
|
|
18
|
+
if (opts?.where) {
|
|
19
|
+
const conditions = buildWhereConditions(opts.where, this.meta);
|
|
20
|
+
if (conditions.length > 0) {
|
|
21
|
+
query = query.where(sql.join(conditions, sql ` AND `));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (opts?.orderBy) {
|
|
25
|
+
for (const [col, dir] of Object.entries(opts.orderBy)) {
|
|
26
|
+
if (dir) {
|
|
27
|
+
const field = this.meta.dbFields.get(col);
|
|
28
|
+
const dbCol = field?.dbName ?? col;
|
|
29
|
+
query = query.orderBy(dbCol, dir);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const limit = opts?.limit ?? 100;
|
|
34
|
+
query = query.limit(limit);
|
|
35
|
+
if (opts?.offset !== undefined) {
|
|
36
|
+
query = query.offset(opts.offset);
|
|
37
|
+
}
|
|
38
|
+
const rows = (await query.execute());
|
|
39
|
+
return rows.map((r) => this.transforms.parseFromDb(r));
|
|
40
|
+
}
|
|
41
|
+
async findById(id) {
|
|
42
|
+
const pkValues = Array.isArray(id) ? id : [id];
|
|
43
|
+
const pkFields = this.meta.pkFields.length > 0
|
|
44
|
+
? this.meta.pkFields
|
|
45
|
+
: Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
|
|
46
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
47
|
+
const qb = this.db;
|
|
48
|
+
const rows = await qb
|
|
49
|
+
.selectFrom(this.meta.tableName)
|
|
50
|
+
.selectAll()
|
|
51
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
52
|
+
.limit(1)
|
|
53
|
+
.execute();
|
|
54
|
+
const row = rows[0] ?? null;
|
|
55
|
+
if (!row)
|
|
56
|
+
return null;
|
|
57
|
+
return this.transforms.parseFromDb(row);
|
|
58
|
+
}
|
|
59
|
+
insert(data, ...args) {
|
|
60
|
+
const dbOnlyData = args[0];
|
|
61
|
+
return {
|
|
62
|
+
ids: () => this.insertIds(data, dbOnlyData),
|
|
63
|
+
full: async () => {
|
|
64
|
+
const ids = await this.insertIds(data, dbOnlyData);
|
|
65
|
+
return this.reconcileIds(data, ids);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async create(data, ...args) {
|
|
70
|
+
const dbOnlyData = args[0];
|
|
71
|
+
return this.insertIds(data, dbOnlyData);
|
|
72
|
+
}
|
|
73
|
+
async insertIds(data, dbOnlyData) {
|
|
74
|
+
const dbData = this.transforms.parseForDb(data);
|
|
75
|
+
const parsedDbOnlyData = this.parseDbOnlyData(dbOnlyData, {
|
|
76
|
+
requireRequired: true,
|
|
77
|
+
});
|
|
78
|
+
const clientPkClientKeys = this.meta.clientPkFields;
|
|
79
|
+
const pkDbNames = new Set(clientPkClientKeys.map((k) => {
|
|
80
|
+
const field = this.meta.dbFields.get(k);
|
|
81
|
+
return field?.dbName ?? k;
|
|
82
|
+
}));
|
|
83
|
+
const insertData = {};
|
|
84
|
+
for (const key of Object.keys(dbData)) {
|
|
85
|
+
if (!pkDbNames.has(key)) {
|
|
86
|
+
insertData[key] = dbData[key];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
Object.assign(insertData, parsedDbOnlyData);
|
|
90
|
+
const qb = this.db;
|
|
91
|
+
const result = await qb
|
|
92
|
+
.insertInto(this.meta.tableName)
|
|
93
|
+
.values(insertData)
|
|
94
|
+
.execute();
|
|
95
|
+
const insertId = result[0]?.insertId;
|
|
96
|
+
if (insertId !== undefined && this.meta.pkFields.length > 0) {
|
|
97
|
+
const dbPkField = this.meta.pkFields[0];
|
|
98
|
+
return { [dbPkField]: Number(insertId) };
|
|
99
|
+
}
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
update(id, data, dbOnlyData) {
|
|
103
|
+
return {
|
|
104
|
+
ids: () => this.updateIds(id, data, dbOnlyData),
|
|
105
|
+
full: async () => {
|
|
106
|
+
const ids = await this.updateIds(id, data, dbOnlyData);
|
|
107
|
+
const idValue = this.firstPkValue(ids);
|
|
108
|
+
const row = await this.findById(idValue);
|
|
109
|
+
if (!row) {
|
|
110
|
+
throw new RecordNotFoundError(this.meta.tableName, idValue);
|
|
111
|
+
}
|
|
112
|
+
return row;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async updateIds(id, data, dbOnlyData) {
|
|
117
|
+
const pkValues = Array.isArray(id) ? id : [id];
|
|
118
|
+
const pkFields = this.meta.pkFields.length > 0
|
|
119
|
+
? this.meta.pkFields
|
|
120
|
+
: Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
|
|
121
|
+
const patchData = data;
|
|
122
|
+
const deriveKeys = this.affectedDbBackedDerives(patchData);
|
|
123
|
+
const missingDeps = this.missingDeriveDependencies(patchData, deriveKeys);
|
|
124
|
+
const fetchedDeps = missingDeps.length > 0
|
|
125
|
+
? await this.fetchClientFieldsById(pkValues, pkFields, missingDeps)
|
|
126
|
+
: {};
|
|
127
|
+
const parseInput = { ...fetchedDeps, ...patchData };
|
|
128
|
+
const parsedDbData = this.transforms.parsePatchForDb(parseInput);
|
|
129
|
+
const dbData = this.pickDbPatchFields(parsedDbData, [
|
|
130
|
+
...Object.keys(patchData),
|
|
131
|
+
...deriveKeys,
|
|
132
|
+
]);
|
|
133
|
+
Object.assign(dbData, this.parseDbOnlyData(dbOnlyData, { requireRequired: false }));
|
|
134
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
135
|
+
const qb = this.db;
|
|
136
|
+
const result = await qb
|
|
137
|
+
.updateTable(this.meta.tableName)
|
|
138
|
+
.set(dbData)
|
|
139
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
140
|
+
.execute();
|
|
141
|
+
const numUpdated = result[0]?.numUpdatedRows ?? 0n;
|
|
142
|
+
if (Number(numUpdated) === 0) {
|
|
143
|
+
throw new RecordNotFoundError(this.meta.tableName, id);
|
|
144
|
+
}
|
|
145
|
+
const pkResult = {};
|
|
146
|
+
for (let i = 0; i < pkFields.length; i++) {
|
|
147
|
+
pkResult[pkFields[i]] = pkValues[i];
|
|
148
|
+
}
|
|
149
|
+
return pkResult;
|
|
150
|
+
}
|
|
151
|
+
affectedDbBackedDerives(patchData) {
|
|
152
|
+
const patchKeys = new Set(Object.keys(patchData));
|
|
153
|
+
const affected = [];
|
|
154
|
+
for (const [deriveKey, deps] of this.meta.deriveDependencies.entries()) {
|
|
155
|
+
if (!this.meta.dbFields.has(deriveKey))
|
|
156
|
+
continue;
|
|
157
|
+
if (deps.some((dep) => patchKeys.has(dep))) {
|
|
158
|
+
affected.push(deriveKey);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return affected;
|
|
162
|
+
}
|
|
163
|
+
missingDeriveDependencies(patchData, deriveKeys) {
|
|
164
|
+
const patchKeys = new Set(Object.keys(patchData));
|
|
165
|
+
const missing = new Set();
|
|
166
|
+
for (const deriveKey of deriveKeys) {
|
|
167
|
+
const deps = this.meta.deriveDependencies.get(deriveKey) ?? [];
|
|
168
|
+
for (const dep of deps) {
|
|
169
|
+
if (!patchKeys.has(dep))
|
|
170
|
+
missing.add(dep);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return Array.from(missing).filter((dep) => this.meta.dbFields.has(dep));
|
|
174
|
+
}
|
|
175
|
+
async fetchClientFieldsById(pkValues, pkFields, clientFields) {
|
|
176
|
+
const dbColumns = clientFields.map((clientKey) => {
|
|
177
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
178
|
+
return field?.dbName ?? clientKey;
|
|
179
|
+
});
|
|
180
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
181
|
+
const qb = this.db;
|
|
182
|
+
const row = (await qb
|
|
183
|
+
.selectFrom(this.meta.tableName)
|
|
184
|
+
.select(dbColumns)
|
|
185
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
186
|
+
.limit(1)
|
|
187
|
+
.executeTakeFirst());
|
|
188
|
+
if (!row) {
|
|
189
|
+
throw new RecordNotFoundError(this.meta.tableName, pkValues);
|
|
190
|
+
}
|
|
191
|
+
const result = {};
|
|
192
|
+
for (const clientKey of clientFields) {
|
|
193
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
194
|
+
const dbName = field?.dbName ?? clientKey;
|
|
195
|
+
const value = row[dbName];
|
|
196
|
+
result[clientKey] = field?.toClient ? field.toClient(value) : value;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
pickDbPatchFields(dbData, clientKeys) {
|
|
201
|
+
const picked = {};
|
|
202
|
+
for (const clientKey of clientKeys) {
|
|
203
|
+
const dbName = this.meta.clientToDbName.get(clientKey) ?? clientKey;
|
|
204
|
+
if (dbData[dbName] !== undefined) {
|
|
205
|
+
picked[dbName] = dbData[dbName];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return picked;
|
|
209
|
+
}
|
|
210
|
+
parseDbOnlyData(dbOnlyData, opts = { requireRequired: false }) {
|
|
211
|
+
if (opts.requireRequired) {
|
|
212
|
+
for (const requiredKey of this.meta.sqlOnlyRequiredClientFields) {
|
|
213
|
+
if (!dbOnlyData || dbOnlyData[requiredKey] === undefined) {
|
|
214
|
+
throw new Error(`Missing required sqlOnly field "${requiredKey}" for "${this.meta.tableName}".`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!dbOnlyData)
|
|
219
|
+
return {};
|
|
220
|
+
const parsed = {};
|
|
221
|
+
for (const [clientKey, value] of Object.entries(dbOnlyData)) {
|
|
222
|
+
if (!this.meta.sqlOnlyClientFields.has(clientKey)) {
|
|
223
|
+
throw new Error(`Field "${clientKey}" is not a sqlOnly field on "${this.meta.tableName}".`);
|
|
224
|
+
}
|
|
225
|
+
const validator = this.meta.sqlOnlyValidators.get(clientKey);
|
|
226
|
+
const validValue = validator ? validator(value) : value;
|
|
227
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
228
|
+
const dbName = field?.dbName ?? clientKey;
|
|
229
|
+
parsed[dbName] = field?.toDb ? field.toDb(validValue) : validValue;
|
|
230
|
+
}
|
|
231
|
+
return parsed;
|
|
232
|
+
}
|
|
233
|
+
reconcileIds(clientData, ids) {
|
|
234
|
+
if (this.reconcile) {
|
|
235
|
+
return this.reconcile(clientData).withServer(ids);
|
|
236
|
+
}
|
|
237
|
+
return this.reconcileFlatIds(clientData, ids);
|
|
238
|
+
}
|
|
239
|
+
reconcileFlatIds(clientData, ids) {
|
|
240
|
+
if (Array.isArray(clientData)) {
|
|
241
|
+
if (!Array.isArray(ids))
|
|
242
|
+
return clientData;
|
|
243
|
+
return clientData.map((item, index) => this.reconcileFlatIds(item, ids[index]));
|
|
244
|
+
}
|
|
245
|
+
if (typeof clientData !== "object" ||
|
|
246
|
+
clientData === null ||
|
|
247
|
+
typeof ids !== "object" ||
|
|
248
|
+
ids === null) {
|
|
249
|
+
return clientData;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
...clientData,
|
|
253
|
+
...this.mapIdsToClientFields(ids),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
mapIdsToClientFields(ids) {
|
|
257
|
+
const mapped = {};
|
|
258
|
+
for (const [idKey, value] of Object.entries(ids)) {
|
|
259
|
+
const clientKey = this.clientKeyForDbField(idKey);
|
|
260
|
+
const field = this.meta.dbFields.get(clientKey);
|
|
261
|
+
mapped[clientKey] = field?.toClient ? field.toClient(value) : value;
|
|
262
|
+
}
|
|
263
|
+
return mapped;
|
|
264
|
+
}
|
|
265
|
+
clientKeyForDbField(dbField) {
|
|
266
|
+
for (const [clientKey, field] of this.meta.dbFields.entries()) {
|
|
267
|
+
if (field.dbName === dbField)
|
|
268
|
+
return clientKey;
|
|
269
|
+
}
|
|
270
|
+
return dbField;
|
|
271
|
+
}
|
|
272
|
+
firstPkValue(ids) {
|
|
273
|
+
const pkField = this.meta.pkFields[0];
|
|
274
|
+
if (pkField && ids[pkField] !== undefined) {
|
|
275
|
+
return ids[pkField];
|
|
276
|
+
}
|
|
277
|
+
return Object.values(ids)[0];
|
|
278
|
+
}
|
|
279
|
+
async delete(id) {
|
|
280
|
+
const pkValues = Array.isArray(id) ? id : [id];
|
|
281
|
+
const pkFields = this.meta.pkFields.length > 0
|
|
282
|
+
? this.meta.pkFields
|
|
283
|
+
: Array.from(this.meta.dbFields.values()).map((f) => f.dbName);
|
|
284
|
+
const conditions = buildPkConditions(pkValues, pkFields);
|
|
285
|
+
const qb = this.db;
|
|
286
|
+
const result = await qb
|
|
287
|
+
.deleteFrom(this.meta.tableName)
|
|
288
|
+
.where(sql.join(conditions, sql ` AND `))
|
|
289
|
+
.execute();
|
|
290
|
+
const numDeleted = result[0]?.numDeletedRows ?? 0n;
|
|
291
|
+
return { deleted: Number(numDeleted) > 0 };
|
|
292
|
+
}
|
|
293
|
+
async count(where) {
|
|
294
|
+
const qb = this.db;
|
|
295
|
+
let query = qb
|
|
296
|
+
.selectFrom(this.meta.tableName)
|
|
297
|
+
.select(sql `count(*)`.as("count"));
|
|
298
|
+
if (where) {
|
|
299
|
+
const conditions = buildWhereConditions(where, this.meta);
|
|
300
|
+
if (conditions.length > 0) {
|
|
301
|
+
query = query.where(sql.join(conditions, sql ` AND `));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const row = (await query.execute());
|
|
305
|
+
return Number(row[0]?.count ?? 0);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type WhereValue<T> = T | {
|
|
2
|
+
contains?: string;
|
|
3
|
+
startsWith?: string;
|
|
4
|
+
endsWith?: string;
|
|
5
|
+
gt?: T;
|
|
6
|
+
gte?: T;
|
|
7
|
+
lt?: T;
|
|
8
|
+
lte?: T;
|
|
9
|
+
in?: T[];
|
|
10
|
+
not?: T | Exclude<WhereValue<T>, T>;
|
|
11
|
+
};
|
|
12
|
+
export type WhereInput<T> = {
|
|
13
|
+
[K in keyof T]?: WhereValue<T[K]>;
|
|
14
|
+
};
|
|
15
|
+
export interface FindManyOpts<T> {
|
|
16
|
+
where?: WhereInput<Partial<T>>;
|
|
17
|
+
orderBy?: {
|
|
18
|
+
[K in keyof T]?: "asc" | "desc";
|
|
19
|
+
};
|
|
20
|
+
limit?: number;
|
|
21
|
+
offset?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ClientToDbField {
|
|
24
|
+
dbName: string;
|
|
25
|
+
toDb?: (val: any) => any;
|
|
26
|
+
toClient?: (val: any) => any;
|
|
27
|
+
}
|
|
28
|
+
export interface TableMeta {
|
|
29
|
+
tableName: string;
|
|
30
|
+
dbFields: Map<string, ClientToDbField>;
|
|
31
|
+
clientToDbName: Map<string, string>;
|
|
32
|
+
pkFields: string[];
|
|
33
|
+
clientPkFields: string[];
|
|
34
|
+
sqlOnlyFields: Set<string>;
|
|
35
|
+
sqlOnlyClientFields: Set<string>;
|
|
36
|
+
sqlOnlyRequiredClientFields: Set<string>;
|
|
37
|
+
sqlOnlyValidators: Map<string, (val: unknown) => unknown>;
|
|
38
|
+
deriveDependencies: Map<string, string[]>;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TableMeta } from "./types.js";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
export declare function buildWhereConditions(filter: Record<string, unknown>, meta: TableMeta): ReturnType<typeof sql>[];
|
|
4
|
+
export declare function buildPkConditions(pkValues: unknown[], pkFields: string[]): ReturnType<typeof sql>[];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
export function buildWhereConditions(filter, meta) {
|
|
3
|
+
const conditions = [];
|
|
4
|
+
for (const [clientKey, value] of Object.entries(filter)) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
continue;
|
|
7
|
+
const field = meta.dbFields.get(clientKey);
|
|
8
|
+
const dbName = field?.dbName ?? clientKey;
|
|
9
|
+
const dbRef = sql.ref(dbName);
|
|
10
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
11
|
+
const op = value;
|
|
12
|
+
if ("contains" in op) {
|
|
13
|
+
const tv = field?.toDb ? field.toDb(op.contains) : op.contains;
|
|
14
|
+
conditions.push(sql `${dbRef} LIKE ${sql.val(`%${String(tv ?? "")}%`)}`);
|
|
15
|
+
}
|
|
16
|
+
else if ("startsWith" in op) {
|
|
17
|
+
const tv = field?.toDb ? field.toDb(op.startsWith) : op.startsWith;
|
|
18
|
+
conditions.push(sql `${dbRef} LIKE ${sql.val(`${String(tv ?? "")}%`)}`);
|
|
19
|
+
}
|
|
20
|
+
else if ("endsWith" in op) {
|
|
21
|
+
const tv = field?.toDb ? field.toDb(op.endsWith) : op.endsWith;
|
|
22
|
+
conditions.push(sql `${dbRef} LIKE ${sql.val(`%${String(tv ?? "")}`)}`);
|
|
23
|
+
}
|
|
24
|
+
else if ("gt" in op) {
|
|
25
|
+
const tv = field?.toDb ? field.toDb(op.gt) : op.gt;
|
|
26
|
+
conditions.push(sql `${dbRef} > ${sql.val(tv)}`);
|
|
27
|
+
}
|
|
28
|
+
else if ("gte" in op) {
|
|
29
|
+
const tv = field?.toDb ? field.toDb(op.gte) : op.gte;
|
|
30
|
+
conditions.push(sql `${dbRef} >= ${sql.val(tv)}`);
|
|
31
|
+
}
|
|
32
|
+
else if ("lt" in op) {
|
|
33
|
+
const tv = field?.toDb ? field.toDb(op.lt) : op.lt;
|
|
34
|
+
conditions.push(sql `${dbRef} < ${sql.val(tv)}`);
|
|
35
|
+
}
|
|
36
|
+
else if ("lte" in op) {
|
|
37
|
+
const tv = field?.toDb ? field.toDb(op.lte) : op.lte;
|
|
38
|
+
conditions.push(sql `${dbRef} <= ${sql.val(tv)}`);
|
|
39
|
+
}
|
|
40
|
+
else if ("in" in op && Array.isArray(op.in)) {
|
|
41
|
+
const items = op.in.map((v) => {
|
|
42
|
+
const tv = field?.toDb ? field.toDb(v) : v;
|
|
43
|
+
return sql.val(tv);
|
|
44
|
+
});
|
|
45
|
+
conditions.push(sql `${dbRef} IN (${sql.join(items, sql `, `)})`);
|
|
46
|
+
}
|
|
47
|
+
else if ("not" in op) {
|
|
48
|
+
const nv = op.not;
|
|
49
|
+
if (nv !== null && typeof nv === "object" && !Array.isArray(nv)) {
|
|
50
|
+
const no = nv;
|
|
51
|
+
if ("contains" in no) {
|
|
52
|
+
const tv = field?.toDb ? field.toDb(no.contains) : no.contains;
|
|
53
|
+
conditions.push(sql `${dbRef} NOT LIKE ${sql.val(`%${String(tv ?? "")}%`)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const tv = field?.toDb ? field.toDb(nv) : nv;
|
|
58
|
+
conditions.push(sql `${dbRef} != ${sql.val(tv)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const transformed = field?.toDb ? field.toDb(value) : value;
|
|
64
|
+
conditions.push(sql `${dbRef} = ${sql.val(transformed)}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return conditions;
|
|
68
|
+
}
|
|
69
|
+
export function buildPkConditions(pkValues, pkFields) {
|
|
70
|
+
return pkFields.map((f, i) => sql `${sql.ref(f)} = ${sql.val(pkValues[i])}`);
|
|
71
|
+
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
export type DeepPartial<T> = T extends object ? {
|
|
3
|
+
[P in keyof T]?: DeepPartial<T[P]>;
|
|
4
|
+
} : T;
|
|
2
5
|
type CurrentTimestampConfig = {
|
|
3
6
|
default: "CURRENT_TIMESTAMP";
|
|
4
7
|
defaultValue: Date;
|
|
@@ -234,6 +237,7 @@ export declare function createSchema<T extends {
|
|
|
234
237
|
}, R extends Record<string, any> = {}, TActualSchema extends Omit<T & R, typeof SchemaWrapperBrand> = Omit<T & R, typeof SchemaWrapperBrand>>(schema: T, relations?: R): {
|
|
235
238
|
pk: string[] | null;
|
|
236
239
|
clientPk: string[] | null;
|
|
240
|
+
deriveDependencies: Record<string, string[]>;
|
|
237
241
|
isClientRecord: (record: any) => boolean;
|
|
238
242
|
sqlSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>;
|
|
239
243
|
clientInputSchema: z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientInputSchema">>>;
|
|
@@ -245,6 +249,7 @@ export declare function createSchema<T extends {
|
|
|
245
249
|
toClient: (dbObject: Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>>;
|
|
246
250
|
toDb: (clientObject: Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>;
|
|
247
251
|
parseForDb: (appData: z.input<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodValidationSchema">>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>;
|
|
252
|
+
parsePatchForDb: (patchData: Partial<z.input<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodValidationSchema">>>>>) => Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>>;
|
|
248
253
|
parseFromDb: (dbData: Partial<z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodSqlSchema">>>>>) => z.infer<z.ZodObject<Prettify<DeriveSchemaByKey<TActualSchema, "zodClientSchema">>>>;
|
|
249
254
|
};
|
|
250
255
|
export type PlaceholderReference = {
|
|
@@ -309,6 +314,7 @@ type ResolvedRegistryWithSchemas<S extends Record<string, SchemaWithPlaceholders
|
|
|
309
314
|
toClient: (dbObject: any) => any;
|
|
310
315
|
toDb: (clientObject: any) => any;
|
|
311
316
|
parseForDb: (appData: any) => any;
|
|
317
|
+
parsePatchForDb: (patchData: any) => any;
|
|
312
318
|
parseFromDb: (dbData: any) => any;
|
|
313
319
|
};
|
|
314
320
|
pk: string[] | null;
|
|
@@ -411,8 +417,12 @@ export type DeriveViewResult<TTableName extends keyof TRegistry, TSelection, TRe
|
|
|
411
417
|
toClient: TRegistry[TTableName]["transforms"]["toClient"];
|
|
412
418
|
toDb: TRegistry[TTableName]["transforms"]["toDb"];
|
|
413
419
|
parseForDb: (appData: z.input<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "serverSchema">>>) => z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>;
|
|
420
|
+
parsePatchForDb: (patchData: Partial<z.input<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "serverSchema">>>>) => Partial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>>;
|
|
414
421
|
parseFromDb: (dbData: Partial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>>) => z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>>;
|
|
415
422
|
};
|
|
423
|
+
reconcile: (clientData: z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>> | z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>>[]) => {
|
|
424
|
+
withServer: (serverData: DeepPartial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>> | DeepPartial<z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "sqlSchema">>>>[]) => z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>> | z.infer<z.ZodObject<_DeriveViewShape<TTableName, TSelection, TRegistry, "clientSchema">>>[];
|
|
425
|
+
};
|
|
416
426
|
defaults: () => DeriveViewDefaults<TTableName, TSelection, TRegistry>;
|
|
417
427
|
defaultsDefinition: () => DeriveViewDefaultsDefinition<TTableName, TSelection, TRegistry>;
|
|
418
428
|
pk: string[] | null;
|
|
@@ -454,19 +464,22 @@ type RegistryShape = Record<string, {
|
|
|
454
464
|
serverSchema: z.ZodObject<any>;
|
|
455
465
|
defaultValues: any;
|
|
456
466
|
stateType: any;
|
|
467
|
+
deriveDependencies: Record<string, string[]>;
|
|
457
468
|
};
|
|
458
469
|
transforms: {
|
|
459
470
|
toClient: (dbObject: any) => any;
|
|
460
471
|
toDb: (clientObject: any) => any;
|
|
461
472
|
parseForDb: (appData: any) => any;
|
|
473
|
+
parsePatchForDb: (patchData: any) => any;
|
|
462
474
|
parseFromDb: (dbData: any) => any;
|
|
463
475
|
};
|
|
464
476
|
pk: string[] | null;
|
|
465
477
|
clientPk: string[] | null;
|
|
478
|
+
deriveDependencies: Record<string, string[]>;
|
|
466
479
|
isClientRecord: (record: any) => boolean;
|
|
467
480
|
generateDefaults: () => any;
|
|
468
481
|
}>;
|
|
469
|
-
type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R extends ResolutionMap<S>, Resolved extends
|
|
482
|
+
type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R extends ResolutionMap<S>, Resolved extends Record<string, any> = ResolvedRegistryWithSchemas<S, R>> = {
|
|
470
483
|
[K in keyof Resolved]: {
|
|
471
484
|
definition: Resolved[K]["rawSchema"];
|
|
472
485
|
schemaKey: K;
|
|
@@ -480,6 +493,7 @@ type CreateSchemaBoxReturn<S extends Record<string, SchemaWithPlaceholders>, R e
|
|
|
480
493
|
toClient: (dbData: z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>;
|
|
481
494
|
toDb: (clientData: z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>;
|
|
482
495
|
parseForDb: (appData: z.input<Resolved[K]["zodSchemas"]["serverSchema"]>) => z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>;
|
|
496
|
+
parsePatchForDb: (patchData: Partial<z.input<Resolved[K]["zodSchemas"]["serverSchema"]>>) => Partial<z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>>;
|
|
483
497
|
parseFromDb: (dbData: Partial<z.infer<Resolved[K]["zodSchemas"]["sqlSchema"]>>) => z.infer<Resolved[K]["zodSchemas"]["clientSchema"]>;
|
|
484
498
|
};
|
|
485
499
|
defaults: Resolved[K]["zodSchemas"]["defaultValues"];
|
package/dist/schema.js
CHANGED
|
@@ -638,9 +638,30 @@ export function createSchema(schema, relations) {
|
|
|
638
638
|
const finalClientInputSchema = z.object(clientInputFields);
|
|
639
639
|
const finalClientSchema = z.object(clientFields);
|
|
640
640
|
const finalValidationSchema = z.object(serverFields);
|
|
641
|
+
const deriveDependencies = {};
|
|
642
|
+
if (derives) {
|
|
643
|
+
const trackingSeed = { ...defaultValues };
|
|
644
|
+
for (const key in derives) {
|
|
645
|
+
const accessed = new Set();
|
|
646
|
+
const trackingRow = new Proxy(trackingSeed, {
|
|
647
|
+
get(target, prop, receiver) {
|
|
648
|
+
if (typeof prop === "string" && prop !== key) {
|
|
649
|
+
accessed.add(prop);
|
|
650
|
+
}
|
|
651
|
+
return Reflect.get(target, prop, receiver);
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
try {
|
|
655
|
+
derives[key](trackingRow);
|
|
656
|
+
}
|
|
657
|
+
catch (e) { }
|
|
658
|
+
deriveDependencies[key] = Array.from(accessed);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
641
661
|
return {
|
|
642
662
|
pk: pkKeys.length ? pkKeys : null,
|
|
643
663
|
clientPk: clientPkKeys.length ? clientPkKeys : null,
|
|
664
|
+
deriveDependencies,
|
|
644
665
|
isClientRecord,
|
|
645
666
|
sqlSchema: finalSqlSchema,
|
|
646
667
|
clientInputSchema: finalClientInputSchema,
|
|
@@ -655,6 +676,10 @@ export function createSchema(schema, relations) {
|
|
|
655
676
|
const validData = finalValidationSchema.parse(appData);
|
|
656
677
|
return toDb(validData);
|
|
657
678
|
},
|
|
679
|
+
parsePatchForDb: (patchData) => {
|
|
680
|
+
const validPatch = finalValidationSchema.partial().parse(patchData);
|
|
681
|
+
return toDb(validPatch);
|
|
682
|
+
},
|
|
658
683
|
parseFromDb: (dbData) => {
|
|
659
684
|
const parsed = finalSqlSchema.parse(dbData);
|
|
660
685
|
return toClient(parsed);
|
|
@@ -798,10 +823,12 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
798
823
|
toClient: zodSchemas.toClient,
|
|
799
824
|
toDb: zodSchemas.toDb,
|
|
800
825
|
parseForDb: zodSchemas.parseForDb,
|
|
826
|
+
parsePatchForDb: zodSchemas.parsePatchForDb,
|
|
801
827
|
parseFromDb: zodSchemas.parseFromDb,
|
|
802
828
|
},
|
|
803
829
|
pk: zodSchemas.pk,
|
|
804
830
|
clientPk: zodSchemas.clientPk,
|
|
831
|
+
deriveDependencies: zodSchemas.deriveDependencies,
|
|
805
832
|
isClientRecord: zodSchemas.isClientRecord,
|
|
806
833
|
generateDefaults: zodSchemas.generateDefaults,
|
|
807
834
|
};
|
|
@@ -877,6 +904,7 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
877
904
|
toClient: entry.transforms.toClient,
|
|
878
905
|
toDb: entry.transforms.toDb,
|
|
879
906
|
parseForDb: entry.transforms.parseForDb,
|
|
907
|
+
parsePatchForDb: entry.transforms.parsePatchForDb,
|
|
880
908
|
parseFromDb: entry.transforms.parseFromDb,
|
|
881
909
|
},
|
|
882
910
|
defaults: entry.generateDefaults(),
|
|
@@ -885,6 +913,7 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
885
913
|
generateDefaults: entry.generateDefaults,
|
|
886
914
|
pk: entry.pk,
|
|
887
915
|
clientPk: entry.clientPk,
|
|
916
|
+
deriveDependencies: entry.deriveDependencies,
|
|
888
917
|
isClientRecord: entry.isClientRecord,
|
|
889
918
|
nav: createNavProxy(tableName, finalRegistry),
|
|
890
919
|
createView: (selection) => {
|
|
@@ -946,6 +975,64 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
946
975
|
};
|
|
947
976
|
const viewToClient = (dbData) => deepToClient(dbData, selection, tableName);
|
|
948
977
|
const viewToDb = (clientData) => deepToDb(clientData, selection, tableName);
|
|
978
|
+
const reconcile = (clientData) => {
|
|
979
|
+
return {
|
|
980
|
+
withServer: (serverData) => {
|
|
981
|
+
const parsedServerData = viewToClient(serverData);
|
|
982
|
+
const mergeTrees = (cNode, sNode, tableKey, sel) => {
|
|
983
|
+
if (sNode === undefined || sNode === null)
|
|
984
|
+
return cNode;
|
|
985
|
+
if (cNode === undefined || cNode === null)
|
|
986
|
+
return sNode;
|
|
987
|
+
const regEntry = finalRegistry[tableKey];
|
|
988
|
+
const clientPkField = regEntry.clientPk?.[0] || regEntry.pk?.[0];
|
|
989
|
+
const dbPkField = regEntry.pk?.[0] || clientPkField;
|
|
990
|
+
if (Array.isArray(cNode)) {
|
|
991
|
+
if (!Array.isArray(sNode))
|
|
992
|
+
return cNode;
|
|
993
|
+
return cNode.map((cItem, index) => {
|
|
994
|
+
let sItem = undefined;
|
|
995
|
+
if (clientPkField && cItem[clientPkField] !== undefined) {
|
|
996
|
+
sItem = sNode.find((s) => s[clientPkField] === cItem[clientPkField]);
|
|
997
|
+
}
|
|
998
|
+
if (!sItem && dbPkField && cItem[dbPkField] !== undefined) {
|
|
999
|
+
sItem = sNode.find((s) => s[dbPkField] === cItem[dbPkField]);
|
|
1000
|
+
}
|
|
1001
|
+
if (!sItem && sNode[index]) {
|
|
1002
|
+
sItem = sNode[index];
|
|
1003
|
+
}
|
|
1004
|
+
return mergeTrees(cItem, sItem, tableKey, sel);
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
if (typeof cNode === "object" && typeof sNode === "object") {
|
|
1008
|
+
const merged = { ...cNode };
|
|
1009
|
+
for (const key in sNode) {
|
|
1010
|
+
const selValue = typeof sel === "object" ? sel[key] : undefined;
|
|
1011
|
+
const relField = regEntry.rawSchema[key];
|
|
1012
|
+
const isRelation = !!(selValue && relField?.config?.sql?.schema);
|
|
1013
|
+
if (isRelation) {
|
|
1014
|
+
const nextTableKey = tableNameToRegistryKeyMap[relField.config.sql.schema()._tableName];
|
|
1015
|
+
merged[key] = mergeTrees(cNode[key], sNode[key], nextTableKey, selValue);
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
merged[key] = sNode[key];
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (clientPkField &&
|
|
1022
|
+
dbPkField &&
|
|
1023
|
+
clientPkField !== dbPkField &&
|
|
1024
|
+
merged[dbPkField] !== undefined &&
|
|
1025
|
+
merged[dbPkField] !== null) {
|
|
1026
|
+
delete merged[clientPkField];
|
|
1027
|
+
}
|
|
1028
|
+
return merged;
|
|
1029
|
+
}
|
|
1030
|
+
return sNode !== undefined ? sNode : cNode;
|
|
1031
|
+
};
|
|
1032
|
+
return mergeTrees(clientData, parsedServerData, tableName, selection);
|
|
1033
|
+
},
|
|
1034
|
+
};
|
|
1035
|
+
};
|
|
949
1036
|
return {
|
|
950
1037
|
definition: entry.rawSchema,
|
|
951
1038
|
schemaKey: tableName,
|
|
@@ -958,16 +1045,19 @@ export function createSchemaBox(schemas, resolutions) {
|
|
|
958
1045
|
toClient: viewToClient,
|
|
959
1046
|
toDb: viewToDb,
|
|
960
1047
|
parseForDb: (appData) => {
|
|
961
|
-
// FIX: Now correctly validates against the view's server schema first
|
|
962
1048
|
const validData = view.server.parse(appData);
|
|
963
1049
|
return viewToDb(validData);
|
|
964
1050
|
},
|
|
1051
|
+
parsePatchForDb: (patchData) => {
|
|
1052
|
+
const validPatch = view.server.partial().parse(patchData);
|
|
1053
|
+
return viewToDb(validPatch);
|
|
1054
|
+
},
|
|
965
1055
|
parseFromDb: (dbData) => {
|
|
966
|
-
// FIX: Now correctly validates against the view's client schema after mapping
|
|
967
1056
|
const mappedData = view.sql.parse(dbData);
|
|
968
1057
|
return viewToClient(mappedData);
|
|
969
1058
|
},
|
|
970
1059
|
},
|
|
1060
|
+
reconcile,
|
|
971
1061
|
defaults: () => computeViewDefaults(tableName, selection, finalRegistry, tableNameToRegistryKeyMap),
|
|
972
1062
|
defaultsDefinition: () => computeViewDefaultsDefinition(tableName, selection, finalRegistry, tableNameToRegistryKeyMap),
|
|
973
1063
|
pk: entry.zodSchemas.pk,
|