cogsbox-shape 0.5.193 → 0.5.194
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -3
- package/cogsbox-shape-db/dist/connect.d.ts +2 -4
- package/dist/schema.d.ts +37 -3
- package/dist/schema.js +67 -3
- package/dist/vitest/fullSchema.test.d.ts +1 -0
- package/dist/vitest/fullSchema.test.js +1367 -0
- package/dist/vitest/generateSQL.test.d.ts +1 -0
- package/dist/vitest/generateSQL.test.js +70 -0
- package/dist/vitest/packageExports.test.d.ts +1 -0
- package/dist/vitest/packageExports.test.js +15 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1367 @@
|
|
|
1
|
+
import { expect, describe, it } from "vitest";
|
|
2
|
+
import { expectTypeOf } from "expect-type";
|
|
3
|
+
// Import the new primary method for schema creation
|
|
4
|
+
import { s, schema, createSchemaBox } from "../schema.js";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
/*
|
|
7
|
+
================================================================
|
|
8
|
+
SECTION A: TYPE-LEVEL TESTS (Using the new pattern)
|
|
9
|
+
================================================================
|
|
10
|
+
*/
|
|
11
|
+
describe("Schema Builder Type Tests (with expect-type)", () => {
|
|
12
|
+
describe("Basic Field Definitions", () => {
|
|
13
|
+
it("should correctly type a simple varchar field", () => {
|
|
14
|
+
const nameField = s.sqlite({ type: "varchar" });
|
|
15
|
+
expectTypeOf(nameField.config.zodSqlSchema).toEqualTypeOf();
|
|
16
|
+
});
|
|
17
|
+
it("should correctly type a nullable integer field", () => {
|
|
18
|
+
const ageField = s.sqlite({ type: "int", nullable: true });
|
|
19
|
+
expectTypeOf(ageField.config.zodClientSchema).toEqualTypeOf();
|
|
20
|
+
});
|
|
21
|
+
it("should correctly type a primary key field", () => {
|
|
22
|
+
const idField = s.sqlite({ type: "int", pk: true });
|
|
23
|
+
expectTypeOf(idField.config.zodSqlSchema).toEqualTypeOf();
|
|
24
|
+
});
|
|
25
|
+
it("should correctly type an enum field", () => {
|
|
26
|
+
const statusField = s.sqlite({
|
|
27
|
+
type: "enum",
|
|
28
|
+
values: ["draft", "published", "archived"],
|
|
29
|
+
});
|
|
30
|
+
expectTypeOf().toEqualTypeOf();
|
|
31
|
+
expect(statusField.config.zodSqlSchema.parse("draft")).toBe("draft");
|
|
32
|
+
expect(() => statusField.config.zodSqlSchema.parse("deleted")).toThrow();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("Chainable Methods", () => {
|
|
36
|
+
it("should create a union type when .client provides a different type", () => {
|
|
37
|
+
const idField = s.sqlite({ type: "int", pk: true }).clientInput({
|
|
38
|
+
value: () => "temp-uuid-123",
|
|
39
|
+
schema: z.literal("temp-uuid-123"),
|
|
40
|
+
});
|
|
41
|
+
expectTypeOf().toEqualTypeOf();
|
|
42
|
+
});
|
|
43
|
+
it("should NOT create a union type when .client provides the same type", () => {
|
|
44
|
+
const countField = s
|
|
45
|
+
.sqlite({ type: "int" })
|
|
46
|
+
.clientInput({ value: () => 0, schema: z.number() });
|
|
47
|
+
expectTypeOf().toEqualTypeOf();
|
|
48
|
+
});
|
|
49
|
+
it("should correctly override the client schema with .clientInput()", () => {
|
|
50
|
+
const statusField = s
|
|
51
|
+
.sqlite({ type: "int" })
|
|
52
|
+
.clientInput(() => z.boolean());
|
|
53
|
+
expectTypeOf().toEqualTypeOf();
|
|
54
|
+
expectTypeOf().toEqualTypeOf();
|
|
55
|
+
expectTypeOf().toEqualTypeOf();
|
|
56
|
+
});
|
|
57
|
+
it("should add validation to client schema with .client()", () => {
|
|
58
|
+
const nameField = s
|
|
59
|
+
.sqlite({ type: "varchar" })
|
|
60
|
+
.clientInput({ value: "John" })
|
|
61
|
+
.client((tools) => tools.clientInput.min(3));
|
|
62
|
+
expectTypeOf().toEqualTypeOf();
|
|
63
|
+
});
|
|
64
|
+
it("should chain .clientInput().client().server() correctly", () => {
|
|
65
|
+
const nameField = s
|
|
66
|
+
.sqlite({ type: "varchar" })
|
|
67
|
+
.clientInput({ value: "" })
|
|
68
|
+
.client((tools) => tools.clientInput.min(3))
|
|
69
|
+
.server((tools) => tools.clientInput.min(5));
|
|
70
|
+
expectTypeOf().toEqualTypeOf();
|
|
71
|
+
expectTypeOf().toEqualTypeOf();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe("`createSchemaBoxRegistry` Integration with Relations", () => {
|
|
75
|
+
// 1. Define schemas with placeholders
|
|
76
|
+
const users = schema({
|
|
77
|
+
_tableName: "users",
|
|
78
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
79
|
+
value: () => "new-user",
|
|
80
|
+
schema: z.literal("new-user"),
|
|
81
|
+
}),
|
|
82
|
+
posts: s.hasMany(),
|
|
83
|
+
});
|
|
84
|
+
const posts = schema({
|
|
85
|
+
_tableName: "posts",
|
|
86
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
87
|
+
isPublished: s.sqlite({ type: "int" }).clientInput(() => z.boolean()),
|
|
88
|
+
authorId: s.reference(() => users.id),
|
|
89
|
+
});
|
|
90
|
+
// 2. Create the registry and resolve relations
|
|
91
|
+
const box = createSchemaBox({ users, posts }, {
|
|
92
|
+
users: {
|
|
93
|
+
posts: { fromKey: "id", toKey: posts.authorId },
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const finalPostResult = box.posts.schemas;
|
|
97
|
+
it("should correctly handle reference types", () => {
|
|
98
|
+
// The authorId should be a union of the DB type (number) and the initial state of the referenced field
|
|
99
|
+
expectTypeOf().toEqualTypeOf();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
/*
|
|
104
|
+
================================================================
|
|
105
|
+
SECTION B: RUNTIME BEHAVIOR TESTS (Using the new pattern)
|
|
106
|
+
================================================================
|
|
107
|
+
*/
|
|
108
|
+
describe("Schema Builder Runtime Behavior", () => {
|
|
109
|
+
describe("Default Value Generation", () => {
|
|
110
|
+
// Define the schema using the new builder syntax
|
|
111
|
+
const defaultsSchema = schema({
|
|
112
|
+
_tableName: "defaults",
|
|
113
|
+
fromInitialState: s.sqlite({ type: "varchar" }).clientInput({
|
|
114
|
+
value: () => "from-initial-state",
|
|
115
|
+
schema: z.string(),
|
|
116
|
+
}),
|
|
117
|
+
fromSqlDefault: s.sqlite({ type: "int", default: 99 }),
|
|
118
|
+
isNullable: s.sqlite({ type: "boolean", nullable: true }),
|
|
119
|
+
hasNoDefault: s.sqlite({ type: "int" }),
|
|
120
|
+
});
|
|
121
|
+
// Process it with the registry
|
|
122
|
+
const box = createSchemaBox({ defaults: defaultsSchema }, {
|
|
123
|
+
defaults: {},
|
|
124
|
+
});
|
|
125
|
+
const defaults = box.defaults.defaults;
|
|
126
|
+
it("should get default from .clientInput()", () => {
|
|
127
|
+
expect(defaults.fromInitialState).toBe("from-initial-state");
|
|
128
|
+
});
|
|
129
|
+
it("should get default from SQL config", () => {
|
|
130
|
+
expect(defaults.fromSqlDefault).toBe(99);
|
|
131
|
+
});
|
|
132
|
+
it("should default a nullable field to null", () => {
|
|
133
|
+
expect(defaults.isNullable).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
it("should use the generated default (e.g., 0 for int) when none is provided", () => {
|
|
136
|
+
expect(defaults.hasNoDefault).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe("Schema Parsing, Validation, and Transformation", () => {
|
|
140
|
+
const complexSchemaDef = schema({
|
|
141
|
+
_tableName: "complex",
|
|
142
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
143
|
+
status: s
|
|
144
|
+
.sqlite({ type: "int" }) // DB: 0=Inactive, 1=Active
|
|
145
|
+
.clientInput(() => z.enum(["inactive", "active"])) // Client: "inactive" | "active"
|
|
146
|
+
.transform({
|
|
147
|
+
toClient: (dbValue) => (dbValue === 1 ? "active" : "inactive"),
|
|
148
|
+
toDb: (clientValue) => (clientValue === "active" ? 1 : 0),
|
|
149
|
+
}),
|
|
150
|
+
name: s
|
|
151
|
+
.sqlite({ type: "varchar" })
|
|
152
|
+
.server(({ sql }) => sql.min(3, "Name is too short")),
|
|
153
|
+
});
|
|
154
|
+
// Use the new registry to process the schema
|
|
155
|
+
const box = createSchemaBox({ complex: complexSchemaDef }, {
|
|
156
|
+
complex: {},
|
|
157
|
+
});
|
|
158
|
+
const { client, sql, server } = box.complex.schemas;
|
|
159
|
+
const { toClient, toDb } = box.complex.transforms;
|
|
160
|
+
it("should correctly transform a DB object to a Client object", () => {
|
|
161
|
+
const dbData = { id: 1, status: 1, name: "Test" };
|
|
162
|
+
const clientResult = toClient(dbData);
|
|
163
|
+
expect(clientResult.status).toBe("active");
|
|
164
|
+
expect(() => client.parse(clientResult)).not.toThrow();
|
|
165
|
+
});
|
|
166
|
+
it("should correctly transform a Client object to a DB object", () => {
|
|
167
|
+
const clientData = { id: 1, status: "inactive", name: "Test" };
|
|
168
|
+
const dbResult = toDb(clientData);
|
|
169
|
+
expect(dbResult.status).toBe(0);
|
|
170
|
+
expect(() => sql.parse(dbResult)).not.toThrow();
|
|
171
|
+
});
|
|
172
|
+
it("should still use the validationSchema for pure validation", () => {
|
|
173
|
+
const invalidClientData = { id: 1, status: "inactive", name: "ab" };
|
|
174
|
+
const result = server.safeParse(invalidClientData);
|
|
175
|
+
expect(result.success).toBe(false);
|
|
176
|
+
if (!result.success) {
|
|
177
|
+
expect(result.error.issues[0].message).toBe("Name is too short");
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe("Tools params are actual Zod schemas at runtime", () => {
|
|
183
|
+
it("should provide actual Zod schemas as tools in .client()", () => {
|
|
184
|
+
let capturedTools;
|
|
185
|
+
s.sqlite({ type: "varchar" })
|
|
186
|
+
.clientInput({ value: "test" })
|
|
187
|
+
.client((tools) => {
|
|
188
|
+
capturedTools = {
|
|
189
|
+
sql: tools.sql,
|
|
190
|
+
clientInput: tools.clientInput,
|
|
191
|
+
client: tools.client,
|
|
192
|
+
};
|
|
193
|
+
return tools.clientInput;
|
|
194
|
+
});
|
|
195
|
+
expect(capturedTools).toBeDefined();
|
|
196
|
+
expect(capturedTools.sql).toBeInstanceOf(z.ZodType);
|
|
197
|
+
expect(typeof capturedTools.sql.parse).toBe("function");
|
|
198
|
+
expect(typeof capturedTools.sql.safeParse).toBe("function");
|
|
199
|
+
expect(capturedTools.clientInput).toBeInstanceOf(z.ZodType);
|
|
200
|
+
expect(typeof capturedTools.clientInput.parse).toBe("function");
|
|
201
|
+
expect(capturedTools.client).toBeInstanceOf(z.ZodType);
|
|
202
|
+
expect(typeof capturedTools.client.parse).toBe("function");
|
|
203
|
+
});
|
|
204
|
+
it("should provide actual Zod schemas as tools in .server()", () => {
|
|
205
|
+
let capturedTools;
|
|
206
|
+
s.sqlite({ type: "varchar" })
|
|
207
|
+
.clientInput({ value: "test" })
|
|
208
|
+
.client((tools) => tools.clientInput)
|
|
209
|
+
.server((tools) => {
|
|
210
|
+
capturedTools = {
|
|
211
|
+
sql: tools.sql,
|
|
212
|
+
clientInput: tools.clientInput,
|
|
213
|
+
client: tools.client,
|
|
214
|
+
};
|
|
215
|
+
return tools.clientInput;
|
|
216
|
+
});
|
|
217
|
+
expect(capturedTools).toBeDefined();
|
|
218
|
+
expect(capturedTools.sql).toBeInstanceOf(z.ZodType);
|
|
219
|
+
expect(capturedTools.clientInput).toBeInstanceOf(z.ZodType);
|
|
220
|
+
expect(capturedTools.client).toBeInstanceOf(z.ZodType);
|
|
221
|
+
});
|
|
222
|
+
it("should allow calling Zod methods on tools params in .client()", () => {
|
|
223
|
+
let clientInputSchema;
|
|
224
|
+
s.sqlite({ type: "varchar" })
|
|
225
|
+
.clientInput({ value: "" })
|
|
226
|
+
.client((tools) => {
|
|
227
|
+
clientInputSchema = tools.clientInput;
|
|
228
|
+
const validated = tools.clientInput.min(3);
|
|
229
|
+
expect(validated.safeParse("ab").success).toBe(false);
|
|
230
|
+
expect(validated.safeParse("abc").success).toBe(true);
|
|
231
|
+
return validated;
|
|
232
|
+
});
|
|
233
|
+
expect(clientInputSchema).toBeInstanceOf(z.ZodString);
|
|
234
|
+
});
|
|
235
|
+
it("should allow calling Zod methods on tools params in .server()", () => {
|
|
236
|
+
let serverClientInput;
|
|
237
|
+
s.sqlite({ type: "varchar" })
|
|
238
|
+
.clientInput({ value: "" })
|
|
239
|
+
.client((tools) => tools.clientInput.min(3))
|
|
240
|
+
.server((tools) => {
|
|
241
|
+
serverClientInput = tools.clientInput;
|
|
242
|
+
const validated = tools.clientInput.min(5);
|
|
243
|
+
expect(validated.safeParse("abcd").success).toBe(false);
|
|
244
|
+
expect(validated.safeParse("abcde").success).toBe(true);
|
|
245
|
+
return validated;
|
|
246
|
+
});
|
|
247
|
+
expect(serverClientInput).toBeInstanceOf(z.ZodString);
|
|
248
|
+
});
|
|
249
|
+
it("should provide correct Zod types for numeric fields in tools", () => {
|
|
250
|
+
let capturedSql;
|
|
251
|
+
let capturedInput;
|
|
252
|
+
s.sqlite({ type: "int" })
|
|
253
|
+
.clientInput({ value: 0 })
|
|
254
|
+
.client((tools) => {
|
|
255
|
+
capturedSql = tools.sql;
|
|
256
|
+
capturedInput = tools.clientInput;
|
|
257
|
+
return tools.clientInput;
|
|
258
|
+
});
|
|
259
|
+
expect(capturedSql).toBeInstanceOf(z.ZodNumber);
|
|
260
|
+
expect(capturedSql.parse(42)).toBe(42);
|
|
261
|
+
expect(capturedInput).toBeInstanceOf(z.ZodNumber);
|
|
262
|
+
expect(capturedInput.parse(0)).toBe(0);
|
|
263
|
+
});
|
|
264
|
+
it("should provide correct Zod types for boolean fields in tools", () => {
|
|
265
|
+
let capturedInput;
|
|
266
|
+
s.sqlite({ type: "int" })
|
|
267
|
+
.clientInput(() => z.boolean())
|
|
268
|
+
.client((tools) => {
|
|
269
|
+
capturedInput = tools.clientInput;
|
|
270
|
+
return tools.clientInput;
|
|
271
|
+
});
|
|
272
|
+
expect(capturedInput).toBeInstanceOf(z.ZodBoolean);
|
|
273
|
+
expect(capturedInput.parse(true)).toBe(true);
|
|
274
|
+
expect(capturedInput.safeParse("not bool").success).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
it("should allow nullable() and other modifiers on tools params", () => {
|
|
277
|
+
let capturedInput;
|
|
278
|
+
s.sqlite({ type: "varchar", nullable: true })
|
|
279
|
+
.clientInput({ value: null, schema: z.string().nullable() })
|
|
280
|
+
.client((tools) => {
|
|
281
|
+
capturedInput = tools.clientInput;
|
|
282
|
+
expect(tools.clientInput.safeParse(null).success).toBe(true);
|
|
283
|
+
expect(tools.clientInput.safeParse("hello").success).toBe(true);
|
|
284
|
+
return tools.clientInput;
|
|
285
|
+
});
|
|
286
|
+
expect(capturedInput).toBeInstanceOf(z.ZodNullable);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
describe("New Session Features - Base Schema Without Relations", () => {
|
|
290
|
+
const users = schema({
|
|
291
|
+
_tableName: "users",
|
|
292
|
+
id: s
|
|
293
|
+
.sqlite({ type: "int", pk: true })
|
|
294
|
+
.clientInput({ value: () => "user-123", schema: z.string() }),
|
|
295
|
+
petId: s.reference(() => pets.id),
|
|
296
|
+
pets: s.hasMany(),
|
|
297
|
+
});
|
|
298
|
+
const pets = schema({
|
|
299
|
+
_tableName: "pets",
|
|
300
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
301
|
+
userId: s.reference(() => users.id),
|
|
302
|
+
owner: s.hasOne(),
|
|
303
|
+
});
|
|
304
|
+
const box = createSchemaBox({ users, pets }, {
|
|
305
|
+
users: {
|
|
306
|
+
pets: { fromKey: "id", toKey: pets.userId },
|
|
307
|
+
},
|
|
308
|
+
pets: {
|
|
309
|
+
owner: { fromKey: "userId", toKey: users.id },
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
describe("Base Schema Excludes Relations", () => {
|
|
313
|
+
it("should exclude relations from base client schema", () => {
|
|
314
|
+
expectTypeOf().toEqualTypeOf();
|
|
315
|
+
// Runtime check - the schema shape should not include 'pets'
|
|
316
|
+
const clientShape = box.users.schemas.client.shape;
|
|
317
|
+
expect(clientShape).not.toHaveProperty("pets");
|
|
318
|
+
expect(clientShape).toHaveProperty("id");
|
|
319
|
+
expect(clientShape).toHaveProperty("petId");
|
|
320
|
+
});
|
|
321
|
+
it("should exclude relations from default values", () => {
|
|
322
|
+
const defaults = box.pets.defaults;
|
|
323
|
+
// Runtime check
|
|
324
|
+
expect(defaults).not.toHaveProperty("owner");
|
|
325
|
+
expect(defaults).toHaveProperty("id");
|
|
326
|
+
expect(defaults).toHaveProperty("userId");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe("View Creation", () => {
|
|
330
|
+
it("should include only selected relations in view", () => {
|
|
331
|
+
const userView = box.users.createView({
|
|
332
|
+
pets: true,
|
|
333
|
+
});
|
|
334
|
+
expectTypeOf().toEqualTypeOf();
|
|
335
|
+
// Runtime check
|
|
336
|
+
const viewShape = userView.schemas.client.shape;
|
|
337
|
+
expect(viewShape).toHaveProperty("pets");
|
|
338
|
+
expect(viewShape.pets).toBeInstanceOf(z.ZodArray);
|
|
339
|
+
});
|
|
340
|
+
it("should handle nested relations correctly", () => {
|
|
341
|
+
const userViewNested = box.users.createView({
|
|
342
|
+
pets: { owner: true },
|
|
343
|
+
});
|
|
344
|
+
expectTypeOf().toEqualTypeOf();
|
|
345
|
+
// Runtime check - owner should not have pets
|
|
346
|
+
const shape = userViewNested.schemas.client.shape;
|
|
347
|
+
if (shape.pets instanceof z.ZodArray) {
|
|
348
|
+
const petSchema = shape.pets.element;
|
|
349
|
+
if (petSchema instanceof z.ZodObject) {
|
|
350
|
+
const petShape = petSchema.shape;
|
|
351
|
+
expect(petShape).toHaveProperty("owner");
|
|
352
|
+
// Check that owner is optional
|
|
353
|
+
expect(petShape.owner).toBeInstanceOf(z.ZodNullable);
|
|
354
|
+
// Check that owner doesn't include pets
|
|
355
|
+
if (petShape.owner instanceof z.ZodNullable) {
|
|
356
|
+
const ownerSchema = petShape.owner._def.innerType;
|
|
357
|
+
if (ownerSchema instanceof z.ZodObject) {
|
|
358
|
+
expect(ownerSchema.shape).not.toHaveProperty("pets");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
it("should provide type-safe navigation", () => {
|
|
366
|
+
expectTypeOf().not.toBeNever();
|
|
367
|
+
// Runtime test - nav proxy should return nested proxies
|
|
368
|
+
expect(box.users.nav).toBeDefined();
|
|
369
|
+
expect(box.users.nav.pets).toBeDefined();
|
|
370
|
+
expect(box.users.nav.pets.owner).toBeDefined();
|
|
371
|
+
});
|
|
372
|
+
describe("Default Values Accessibility", () => {
|
|
373
|
+
it("should expose default values at top level", () => {
|
|
374
|
+
expect(box.users.defaults).toBeDefined();
|
|
375
|
+
expect(box.users.defaults.id).toBe("user-123");
|
|
376
|
+
expect(box.pets.defaults.id).toBe(0);
|
|
377
|
+
expect(box.pets.defaults.userId).toBe("user-123");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
describe("Reference Resolution", () => {
|
|
381
|
+
it("should correctly resolve reference types", () => {
|
|
382
|
+
expectTypeOf().toEqualTypeOf();
|
|
383
|
+
expectTypeOf().toEqualTypeOf();
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
describe("Relation Defaults in Views", () => {
|
|
388
|
+
const users = schema({
|
|
389
|
+
_tableName: "users",
|
|
390
|
+
id: s
|
|
391
|
+
.sqlite({ type: "int", pk: true })
|
|
392
|
+
.clientInput({ value: () => "user-123", schema: z.string() }),
|
|
393
|
+
name: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
394
|
+
posts: s.hasMany({ count: 2 }), // Should generate 2 posts
|
|
395
|
+
comments: s.hasMany([]), // Should be empty array
|
|
396
|
+
profile: s.hasOne(true), // Changed from {} to true
|
|
397
|
+
settings: s.hasOne(null), // Should be null
|
|
398
|
+
followers: s.hasMany(undefined), // Should not be included
|
|
399
|
+
});
|
|
400
|
+
const posts = schema({
|
|
401
|
+
_tableName: "posts",
|
|
402
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
403
|
+
title: s.sqlite({ type: "varchar" }).clientInput({ value: "Default Post" }),
|
|
404
|
+
authorId: s.reference(() => users.id),
|
|
405
|
+
user: s.hasOne(true),
|
|
406
|
+
});
|
|
407
|
+
const comments = schema({
|
|
408
|
+
_tableName: "comments",
|
|
409
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
410
|
+
text: s
|
|
411
|
+
.sqlite({ type: "varchar" })
|
|
412
|
+
.clientInput({ value: "Default Comment" }),
|
|
413
|
+
userId: s.reference(() => users.id),
|
|
414
|
+
user: s.hasOne(true),
|
|
415
|
+
});
|
|
416
|
+
const profiles = schema({
|
|
417
|
+
_tableName: "profiles",
|
|
418
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
419
|
+
bio: s.sqlite({ type: "varchar" }).clientInput({ value: "Default Bio" }),
|
|
420
|
+
userId: s.reference(() => users.id),
|
|
421
|
+
user: s.hasOne(true),
|
|
422
|
+
});
|
|
423
|
+
const box = createSchemaBox({ users, posts, comments, profiles }, {
|
|
424
|
+
users: {
|
|
425
|
+
posts: { fromKey: "id", toKey: posts.authorId },
|
|
426
|
+
comments: { fromKey: "id", toKey: comments.userId },
|
|
427
|
+
profile: { fromKey: "id", toKey: profiles.userId },
|
|
428
|
+
settings: { fromKey: "id", toKey: profiles.userId },
|
|
429
|
+
followers: { fromKey: "id", toKey: users.id },
|
|
430
|
+
},
|
|
431
|
+
posts: {
|
|
432
|
+
user: { fromKey: "id", toKey: users.id },
|
|
433
|
+
},
|
|
434
|
+
comments: {
|
|
435
|
+
user: { fromKey: "id", toKey: users.id },
|
|
436
|
+
},
|
|
437
|
+
profiles: {
|
|
438
|
+
user: { fromKey: "id", toKey: users.id },
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
it("should generate correct defaults based on relation config", () => {
|
|
442
|
+
const view = box.users.createView({
|
|
443
|
+
posts: true,
|
|
444
|
+
comments: true,
|
|
445
|
+
profile: true,
|
|
446
|
+
settings: true,
|
|
447
|
+
followers: true,
|
|
448
|
+
});
|
|
449
|
+
const defaults = view.defaults();
|
|
450
|
+
// Base fields
|
|
451
|
+
expect(defaults.id).toBe("user-123");
|
|
452
|
+
expect(defaults.name).toBe("John");
|
|
453
|
+
// posts with count: 2
|
|
454
|
+
expect(defaults.posts).toHaveLength(2);
|
|
455
|
+
expect(defaults.posts?.[0]).toEqual({
|
|
456
|
+
id: 0,
|
|
457
|
+
title: "Default Post",
|
|
458
|
+
authorId: "user-123",
|
|
459
|
+
});
|
|
460
|
+
expect(defaults.posts?.[1]).toEqual({
|
|
461
|
+
id: 0,
|
|
462
|
+
title: "Default Post",
|
|
463
|
+
authorId: "user-123",
|
|
464
|
+
});
|
|
465
|
+
// comments with []
|
|
466
|
+
expect(defaults.comments).toEqual([]);
|
|
467
|
+
// profile with {}
|
|
468
|
+
expect(defaults.profile).toEqual({
|
|
469
|
+
id: 0,
|
|
470
|
+
bio: "Default Bio",
|
|
471
|
+
userId: "user-123",
|
|
472
|
+
});
|
|
473
|
+
// settings with null
|
|
474
|
+
expect(defaults.settings).toBeNull();
|
|
475
|
+
// followers with undefined - should not exist
|
|
476
|
+
expect(defaults).not.toHaveProperty("followers");
|
|
477
|
+
});
|
|
478
|
+
it("should handle nested relation defaults", () => {
|
|
479
|
+
const nestedView = box.users.createView({
|
|
480
|
+
posts: {},
|
|
481
|
+
});
|
|
482
|
+
const defaults = nestedView.defaults();
|
|
483
|
+
expect(defaults.posts).toHaveLength(2);
|
|
484
|
+
expect(defaults.posts?.[0]).not.toHaveProperty("author"); // Relation not selected
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
describe("Transform affects defaults", () => {
|
|
488
|
+
const userSchema = schema({
|
|
489
|
+
_tableName: "users",
|
|
490
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
491
|
+
value: () => `temp_${Math.random().toString(36).substr(2, 9)}`,
|
|
492
|
+
schema: z.string(),
|
|
493
|
+
}),
|
|
494
|
+
email: s
|
|
495
|
+
.sqlite({ type: "varchar", length: 255 })
|
|
496
|
+
.server(() => z.email("Invalid email address")),
|
|
497
|
+
isActive: s
|
|
498
|
+
.sqlite({ type: "int" })
|
|
499
|
+
.clientInput(() => z.boolean())
|
|
500
|
+
.transform({
|
|
501
|
+
toClient: (val) => val === 1,
|
|
502
|
+
toDb: (val) => (val ? 1 : 0),
|
|
503
|
+
}),
|
|
504
|
+
role: s.sqlite({ type: "varchar" }).clientInput({
|
|
505
|
+
value: "user",
|
|
506
|
+
schema: z.enum(["user", "admin"]),
|
|
507
|
+
}),
|
|
508
|
+
});
|
|
509
|
+
const box = createSchemaBox({ users: userSchema }, { users: {} });
|
|
510
|
+
const { transforms } = box.users;
|
|
511
|
+
const defaults = box.users.generateDefaults();
|
|
512
|
+
it("should have defaults with correct runtime values", () => {
|
|
513
|
+
expect(defaults.isActive).toBe(false);
|
|
514
|
+
expect(typeof defaults.isActive).toBe("boolean");
|
|
515
|
+
});
|
|
516
|
+
it("should correctly transform defaults to db format", () => {
|
|
517
|
+
const dbVersion = transforms.toDb(defaults);
|
|
518
|
+
expect(dbVersion.isActive).toBe(0);
|
|
519
|
+
expect(typeof dbVersion.isActive).toBe("number");
|
|
520
|
+
const clientVersion = transforms.toClient(dbVersion);
|
|
521
|
+
expect(clientVersion.isActive).toBe(false);
|
|
522
|
+
expect(typeof clientVersion.isActive).toBe("boolean");
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
describe("UUID generation in initialState", () => {
|
|
526
|
+
it("should call value function with uuid tool", () => {
|
|
527
|
+
const field = s.sqlite({ type: "int", pk: true }).clientInput({
|
|
528
|
+
value: ({ uuid }) => uuid(),
|
|
529
|
+
schema: z.string(),
|
|
530
|
+
clientPk: true,
|
|
531
|
+
});
|
|
532
|
+
expect(typeof field.config.initialValue).toBe("function");
|
|
533
|
+
const result = field.config.initialValue({
|
|
534
|
+
uuid: () => "test-uuid",
|
|
535
|
+
});
|
|
536
|
+
expect(typeof result).toBe("string");
|
|
537
|
+
expect(result.length).toBeGreaterThan(0);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
describe("Missing properties - parseForDb, parseFromDb, pk, clientPk, isClientRecord", () => {
|
|
541
|
+
const users = schema({
|
|
542
|
+
_tableName: "users",
|
|
543
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
544
|
+
value: ({ uuid }) => uuid(),
|
|
545
|
+
schema: z.string(),
|
|
546
|
+
clientPk: true,
|
|
547
|
+
}),
|
|
548
|
+
name: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
549
|
+
isActive: s
|
|
550
|
+
.sqlite({ type: "int" })
|
|
551
|
+
.clientInput(() => z.boolean())
|
|
552
|
+
.transform({
|
|
553
|
+
toClient: (val) => val === 1,
|
|
554
|
+
toDb: (val) => (val ? 1 : 0),
|
|
555
|
+
}),
|
|
556
|
+
email: s.sqlite({ type: "varchar", field: "email_address" }),
|
|
557
|
+
posts: s.hasMany({ count: 1 }),
|
|
558
|
+
});
|
|
559
|
+
const posts = schema({
|
|
560
|
+
_tableName: "posts",
|
|
561
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
562
|
+
title: s.sqlite({ type: "varchar" }).clientInput({ value: "Untitled" }),
|
|
563
|
+
authorId: s.reference(() => users.id),
|
|
564
|
+
});
|
|
565
|
+
const box = createSchemaBox({ users, posts }, {
|
|
566
|
+
users: {
|
|
567
|
+
posts: { fromKey: "id", toKey: posts.authorId },
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
describe("DB Field Key Mapping (field property)", () => {
|
|
571
|
+
it("should correctly map 'field' property to sqlSchema types and runtime shapes", () => {
|
|
572
|
+
expectTypeOf().toHaveProperty("email_address");
|
|
573
|
+
expectTypeOf().not.toHaveProperty("email");
|
|
574
|
+
expectTypeOf().toHaveProperty("email");
|
|
575
|
+
expectTypeOf().not.toHaveProperty("email_address");
|
|
576
|
+
const sqlKeys = Object.keys(box.users.schemas.sql.shape);
|
|
577
|
+
expect(sqlKeys).toContain("email_address");
|
|
578
|
+
expect(sqlKeys).not.toContain("email");
|
|
579
|
+
const clientKeys = Object.keys(box.users.schemas.client.shape);
|
|
580
|
+
expect(clientKeys).toContain("email");
|
|
581
|
+
expect(clientKeys).not.toContain("email_address");
|
|
582
|
+
});
|
|
583
|
+
it("parseForDb should return an object with the DB keys typed correctly", () => {
|
|
584
|
+
const clientData = {
|
|
585
|
+
id: 1,
|
|
586
|
+
name: "Alice",
|
|
587
|
+
isActive: true,
|
|
588
|
+
email: "alice@test.com",
|
|
589
|
+
};
|
|
590
|
+
const dbData = box.users.transforms.parseForDb(clientData);
|
|
591
|
+
expectTypeOf(dbData).toHaveProperty("email_address");
|
|
592
|
+
expectTypeOf(dbData).not.toHaveProperty("email");
|
|
593
|
+
expect(dbData.email_address).toBe("alice@test.com");
|
|
594
|
+
expect(dbData.email).toBeUndefined();
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
describe("pk and clientPk", () => {
|
|
598
|
+
it("should expose pk array on base schema", () => {
|
|
599
|
+
expect(box.users.pk).toEqual(["id"]);
|
|
600
|
+
expect(box.posts.pk).toEqual(["id"]);
|
|
601
|
+
});
|
|
602
|
+
it("should expose clientPk array for fields with clientPk: true", () => {
|
|
603
|
+
expect(box.users.clientPk).toEqual(["id"]);
|
|
604
|
+
});
|
|
605
|
+
it("should inherit clientPk on reference fields", () => {
|
|
606
|
+
expect(box.posts.clientPk).toEqual(["authorId"]);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
describe("isClientRecord", () => {
|
|
610
|
+
it("should auto-detect client records when clientPk is string but sql is int", () => {
|
|
611
|
+
expect(box.users.isClientRecord).toBeDefined();
|
|
612
|
+
expect(typeof box.users.isClientRecord).toBe("function");
|
|
613
|
+
expect(box.users.isClientRecord({ id: "some-uuid", name: "Test" })).toBe(true);
|
|
614
|
+
expect(box.users.isClientRecord({ id: 42, name: "Test" })).toBe(false);
|
|
615
|
+
});
|
|
616
|
+
it("should also have isClientRecord on posts due to inherited clientPk from reference", () => {
|
|
617
|
+
expect(box.posts.isClientRecord).toBeDefined();
|
|
618
|
+
expect(typeof box.posts.isClientRecord).toBe("function");
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
describe("parseFromDb and parseForDb", () => {
|
|
622
|
+
it("should map DB column names to client keys on parseFromDb", () => {
|
|
623
|
+
const dbRow = {
|
|
624
|
+
id: 1,
|
|
625
|
+
name: "Alice",
|
|
626
|
+
isActive: 1,
|
|
627
|
+
email_address: "a@b.com",
|
|
628
|
+
};
|
|
629
|
+
const result = box.users.transforms.parseFromDb(dbRow);
|
|
630
|
+
expect(result.email).toBe("a@b.com");
|
|
631
|
+
expect(result.isActive).toBe(true);
|
|
632
|
+
});
|
|
633
|
+
it("should map client keys to DB column names on parseForDb", () => {
|
|
634
|
+
const clientData = {
|
|
635
|
+
id: 1,
|
|
636
|
+
name: "Alice",
|
|
637
|
+
isActive: true,
|
|
638
|
+
email: "a@b.com",
|
|
639
|
+
};
|
|
640
|
+
const result = box.users.transforms.parseForDb(clientData);
|
|
641
|
+
expect(result.isActive).toBe(1);
|
|
642
|
+
expect(result.email_address).toBe("a@b.com");
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
// =======================================================
|
|
647
|
+
// NEW TEST SUITE: Smart ClientPK & Mapped DB Keys Logic
|
|
648
|
+
// =======================================================
|
|
649
|
+
describe("Smart clientPk and isClientRecord logic", () => {
|
|
650
|
+
const smartSchema = schema({
|
|
651
|
+
_tableName: "smart_table",
|
|
652
|
+
// 1. Auto-detect function execution
|
|
653
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
654
|
+
value: ({ uuid }) => uuid(),
|
|
655
|
+
schema: z.string(),
|
|
656
|
+
clientPk: true, // Should auto-detect by dummy-executing the uuid factory
|
|
657
|
+
}),
|
|
658
|
+
// 2. Custom function + mapped db key
|
|
659
|
+
mappedId: s.sqlite({ type: "int", field: "db_mapped_id" }).clientInput({
|
|
660
|
+
value: "temp_999",
|
|
661
|
+
schema: z.string(),
|
|
662
|
+
clientPk: (val) => typeof val === "string" && val.startsWith("temp_"),
|
|
663
|
+
}),
|
|
664
|
+
});
|
|
665
|
+
const smartBox = createSchemaBox({ smart: smartSchema }, {
|
|
666
|
+
smart: {},
|
|
667
|
+
});
|
|
668
|
+
it("should auto-detect client records by dummy-executing the factory function", () => {
|
|
669
|
+
// Because value: ({ uuid }) => uuid() returns a string, the system should auto-infer `typeof val === "string"`
|
|
670
|
+
expect(smartBox.smart.isClientRecord({ id: "some-uuid-123" })).toBe(true);
|
|
671
|
+
expect(smartBox.smart.isClientRecord({ id: 1 })).toBe(false);
|
|
672
|
+
});
|
|
673
|
+
it("should use a custom checker function if provided to clientPk", () => {
|
|
674
|
+
// Valid custom format
|
|
675
|
+
expect(smartBox.smart.isClientRecord({ mappedId: "temp_abc" })).toBe(true);
|
|
676
|
+
// Invalid custom format (doesn't start with temp_)
|
|
677
|
+
expect(smartBox.smart.isClientRecord({ mappedId: "invalid_abc" })).toBe(false);
|
|
678
|
+
// Integer is not a client record
|
|
679
|
+
expect(smartBox.smart.isClientRecord({ mappedId: 42 })).toBe(false);
|
|
680
|
+
});
|
|
681
|
+
it("should safely check BOTH the client key and the db key at runtime", () => {
|
|
682
|
+
// Provide ONLY the client key
|
|
683
|
+
expect(smartBox.smart.isClientRecord({ mappedId: "temp_client" })).toBe(true);
|
|
684
|
+
// Provide ONLY the db key (how it might look coming straight from a SQL query)
|
|
685
|
+
expect(smartBox.smart.isClientRecord({ db_mapped_id: "temp_db" })).toBe(true);
|
|
686
|
+
// Ensure failures correctly resolve when testing either key
|
|
687
|
+
expect(smartBox.smart.isClientRecord({ mappedId: 100 })).toBe(false);
|
|
688
|
+
expect(smartBox.smart.isClientRecord({ db_mapped_id: 100 })).toBe(false);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
describe("Nested relations with transforms", () => {
|
|
692
|
+
const users = schema({
|
|
693
|
+
_tableName: "users",
|
|
694
|
+
id: s
|
|
695
|
+
.sqlite({ type: "int", pk: true })
|
|
696
|
+
.clientInput({ value: () => "user-123", schema: z.string() }),
|
|
697
|
+
name: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
698
|
+
posts: s.hasMany({ count: 1 }),
|
|
699
|
+
});
|
|
700
|
+
const posts = schema({
|
|
701
|
+
_tableName: "posts",
|
|
702
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
703
|
+
title: s.sqlite({ type: "varchar" }).clientInput({ value: "Default Post" }),
|
|
704
|
+
isPublished: s
|
|
705
|
+
.sqlite({ type: "int" })
|
|
706
|
+
.clientInput(() => z.boolean())
|
|
707
|
+
.transform({
|
|
708
|
+
toClient: (val) => val === 1,
|
|
709
|
+
toDb: (val) => (val ? 1 : 0),
|
|
710
|
+
}),
|
|
711
|
+
authorId: s.reference(() => users.id),
|
|
712
|
+
comments: s.hasMany({ count: 1 }),
|
|
713
|
+
});
|
|
714
|
+
const comments = schema({
|
|
715
|
+
_tableName: "comments",
|
|
716
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
717
|
+
text: s.sqlite({ type: "varchar" }).clientInput({ value: "Comment" }),
|
|
718
|
+
isDeleted: s
|
|
719
|
+
.sqlite({ type: "int" })
|
|
720
|
+
.clientInput(() => z.boolean())
|
|
721
|
+
.transform({
|
|
722
|
+
toClient: (val) => val === 1,
|
|
723
|
+
toDb: (val) => (val ? 1 : 0),
|
|
724
|
+
}),
|
|
725
|
+
postId: s.reference(() => posts.id),
|
|
726
|
+
});
|
|
727
|
+
const box = createSchemaBox({ users, posts, comments }, {
|
|
728
|
+
users: {
|
|
729
|
+
posts: { fromKey: "id", toKey: posts.authorId },
|
|
730
|
+
},
|
|
731
|
+
posts: {
|
|
732
|
+
comments: { fromKey: "id", toKey: comments.postId },
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
it("should transform nested relation fields in defaults", () => {
|
|
736
|
+
const view = box.users.createView({
|
|
737
|
+
posts: { comments: true },
|
|
738
|
+
});
|
|
739
|
+
const defaults = view.defaults();
|
|
740
|
+
expect(defaults.posts).toHaveLength(1);
|
|
741
|
+
const post = defaults.posts?.[0];
|
|
742
|
+
expect(post).toBeDefined();
|
|
743
|
+
expect(post.isPublished).toBe(false);
|
|
744
|
+
expect(typeof post.isPublished).toBe("boolean");
|
|
745
|
+
expect(post.comments).toHaveLength(1);
|
|
746
|
+
const comment = post.comments?.[0];
|
|
747
|
+
expect(comment.isDeleted).toBe(false);
|
|
748
|
+
expect(typeof comment.isDeleted).toBe("boolean");
|
|
749
|
+
});
|
|
750
|
+
it("should transform nested relation fields in toClient", () => {
|
|
751
|
+
const view = box.users.createView({
|
|
752
|
+
posts: { comments: true },
|
|
753
|
+
});
|
|
754
|
+
const { toClient } = view.transforms;
|
|
755
|
+
const dbData = {
|
|
756
|
+
id: 1,
|
|
757
|
+
name: "John",
|
|
758
|
+
posts: [
|
|
759
|
+
{
|
|
760
|
+
id: 10,
|
|
761
|
+
title: "My Post",
|
|
762
|
+
isPublished: 1,
|
|
763
|
+
authorId: 1,
|
|
764
|
+
comments: [
|
|
765
|
+
{
|
|
766
|
+
id: 100,
|
|
767
|
+
text: "Comment text",
|
|
768
|
+
isDeleted: 0,
|
|
769
|
+
postId: 10,
|
|
770
|
+
},
|
|
771
|
+
],
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
};
|
|
775
|
+
const clientData = toClient(dbData);
|
|
776
|
+
expect(clientData.posts[0].isPublished).toBe(true);
|
|
777
|
+
expect(typeof clientData.posts[0].isPublished).toBe("boolean");
|
|
778
|
+
expect(clientData.posts[0].comments[0].isDeleted).toBe(false);
|
|
779
|
+
expect(typeof clientData.posts[0].comments[0].isDeleted).toBe("boolean");
|
|
780
|
+
});
|
|
781
|
+
it("should transform nested relation fields in toDb", () => {
|
|
782
|
+
const view = box.users.createView({
|
|
783
|
+
posts: { comments: true },
|
|
784
|
+
});
|
|
785
|
+
const { toDb } = view.transforms;
|
|
786
|
+
const clientData = {
|
|
787
|
+
id: "user-123",
|
|
788
|
+
name: "John",
|
|
789
|
+
posts: [
|
|
790
|
+
{
|
|
791
|
+
id: 10,
|
|
792
|
+
title: "My Post",
|
|
793
|
+
isPublished: true,
|
|
794
|
+
authorId: "user-123",
|
|
795
|
+
comments: [
|
|
796
|
+
{
|
|
797
|
+
id: 100,
|
|
798
|
+
text: "Comment text",
|
|
799
|
+
isDeleted: true,
|
|
800
|
+
postId: 10,
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
},
|
|
804
|
+
],
|
|
805
|
+
};
|
|
806
|
+
const dbData = toDb(clientData);
|
|
807
|
+
expect(dbData.posts[0].isPublished).toBe(1);
|
|
808
|
+
expect(typeof dbData.posts[0].isPublished).toBe("number");
|
|
809
|
+
expect(dbData.posts[0].comments[0].isDeleted).toBe(1);
|
|
810
|
+
expect(typeof dbData.posts[0].comments[0].isDeleted).toBe("number");
|
|
811
|
+
});
|
|
812
|
+
it("should work with parseFromDb and parseForDb", () => {
|
|
813
|
+
const view = box.users.createView({
|
|
814
|
+
posts: { comments: true },
|
|
815
|
+
});
|
|
816
|
+
const { parseFromDb, parseForDb } = view.transforms;
|
|
817
|
+
const dbRow = {
|
|
818
|
+
id: 1,
|
|
819
|
+
name: "John",
|
|
820
|
+
posts: [
|
|
821
|
+
{
|
|
822
|
+
id: 10,
|
|
823
|
+
title: "My Post",
|
|
824
|
+
isPublished: 1,
|
|
825
|
+
authorId: 1,
|
|
826
|
+
comments: [
|
|
827
|
+
{
|
|
828
|
+
id: 100,
|
|
829
|
+
text: "Comment text",
|
|
830
|
+
isDeleted: 0,
|
|
831
|
+
postId: 10,
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
},
|
|
835
|
+
],
|
|
836
|
+
};
|
|
837
|
+
const clientResult = parseFromDb(dbRow);
|
|
838
|
+
expect(clientResult.id).toBe(1);
|
|
839
|
+
expect(clientResult.posts[0].isPublished).toBe(true);
|
|
840
|
+
expect(clientResult.posts[0].comments[0].isDeleted).toBe(false);
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
describe("sqlOnly fields", () => {
|
|
844
|
+
const users = schema({
|
|
845
|
+
_tableName: "users",
|
|
846
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
847
|
+
value: () => "user-123",
|
|
848
|
+
schema: z.string(),
|
|
849
|
+
}),
|
|
850
|
+
name: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
851
|
+
internalToken: s.sqlite({ type: "varchar", sqlOnly: true }),
|
|
852
|
+
});
|
|
853
|
+
const box = createSchemaBox({ users }, { users: {} });
|
|
854
|
+
it("should exclude sqlOnly fields from client schema", () => {
|
|
855
|
+
expectTypeOf().not.toHaveProperty("internalToken");
|
|
856
|
+
const clientKeys = Object.keys(box.users.schemas.client.shape);
|
|
857
|
+
expect(clientKeys).not.toContain("internalToken");
|
|
858
|
+
});
|
|
859
|
+
it("should include sqlOnly fields in sql schema", () => {
|
|
860
|
+
expectTypeOf().toHaveProperty("internalToken");
|
|
861
|
+
const sqlKeys = Object.keys(box.users.schemas.sql.shape);
|
|
862
|
+
expect(sqlKeys).toContain("internalToken");
|
|
863
|
+
});
|
|
864
|
+
it("should exclude sqlOnly fields from defaults", () => {
|
|
865
|
+
const defaults = box.users.defaults;
|
|
866
|
+
expect(defaults).not.toHaveProperty("internalToken");
|
|
867
|
+
});
|
|
868
|
+
it("should exclude sqlOnly fields from toClient output", () => {
|
|
869
|
+
const { toClient } = box.users.transforms;
|
|
870
|
+
const dbData = { id: 1, name: "John", internalToken: "secret" };
|
|
871
|
+
const result = toClient(dbData);
|
|
872
|
+
expect(result).not.toHaveProperty("internalToken");
|
|
873
|
+
});
|
|
874
|
+
it("should NOT include sqlOnly fields in toDb output", () => {
|
|
875
|
+
const { toDb } = box.users.transforms;
|
|
876
|
+
const clientData = { id: 1, name: "John" };
|
|
877
|
+
const result = toDb(clientData);
|
|
878
|
+
expect(result).not.toHaveProperty("internalToken");
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
describe("SQL enum fields", () => {
|
|
882
|
+
const posts = schema({
|
|
883
|
+
_tableName: "posts",
|
|
884
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
885
|
+
status: s.sqlite({
|
|
886
|
+
type: "enum",
|
|
887
|
+
values: ["draft", "published", "archived"],
|
|
888
|
+
default: "draft",
|
|
889
|
+
}),
|
|
890
|
+
nullableStatus: s.sqlite({
|
|
891
|
+
type: "enum",
|
|
892
|
+
values: ["draft", "published"],
|
|
893
|
+
nullable: true,
|
|
894
|
+
}),
|
|
895
|
+
});
|
|
896
|
+
const box = createSchemaBox({ posts }, { posts: {} });
|
|
897
|
+
it("should validate enum values in sql, client, and server schemas", () => {
|
|
898
|
+
expect(box.posts.schemas.sql.shape.status.parse("published")).toBe("published");
|
|
899
|
+
expect(box.posts.schemas.client.shape.status.parse("archived")).toBe("archived");
|
|
900
|
+
expect(box.posts.schemas.server.shape.status.parse("draft")).toBe("draft");
|
|
901
|
+
expect(() => box.posts.schemas.server.shape.status.parse("deleted")).toThrow();
|
|
902
|
+
});
|
|
903
|
+
it("should use enum defaults and nullable enum defaults", () => {
|
|
904
|
+
expect(box.posts.defaults.status).toBe("draft");
|
|
905
|
+
expect(box.posts.defaults.nullableStatus).toBeNull();
|
|
906
|
+
});
|
|
907
|
+
it("should parse enum data for db writes", () => {
|
|
908
|
+
expect(box.posts.transforms.parseForDb({
|
|
909
|
+
id: 1,
|
|
910
|
+
status: "published",
|
|
911
|
+
nullableStatus: null,
|
|
912
|
+
})).toEqual({ id: 1, status: "published", nullableStatus: null });
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
describe("derive - computed fields", () => {
|
|
916
|
+
const users = schema({
|
|
917
|
+
_tableName: "users",
|
|
918
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
919
|
+
fullName: s.clientInput(""),
|
|
920
|
+
firstName: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
921
|
+
lastName: s.sqlite({ type: "varchar" }).clientInput({ value: "Doe" }),
|
|
922
|
+
}).derive({
|
|
923
|
+
forClient: {
|
|
924
|
+
fullName: (row) => `${row.firstName} ${row.lastName}`,
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
const box = createSchemaBox({ users }, { users: {} });
|
|
928
|
+
it("should add derived fields to defaults", () => {
|
|
929
|
+
const defaults = box.users.defaults;
|
|
930
|
+
expect(defaults.fullName).toBe("John Doe");
|
|
931
|
+
});
|
|
932
|
+
it("should compute derived fields in toClient", () => {
|
|
933
|
+
const { toClient } = box.users.transforms;
|
|
934
|
+
const dbData = { id: 1, firstName: "Jane", lastName: "Smith" };
|
|
935
|
+
const result = toClient(dbData);
|
|
936
|
+
expect(result.fullName).toBe("Jane Smith");
|
|
937
|
+
});
|
|
938
|
+
it("should NOT include derived fields in sql schema", () => {
|
|
939
|
+
const sqlKeys = Object.keys(box.users.schemas.sql.shape);
|
|
940
|
+
expect(sqlKeys).not.toContain("fullName");
|
|
941
|
+
});
|
|
942
|
+
it("should include derived fields in client schema", () => {
|
|
943
|
+
expectTypeOf().toHaveProperty("fullName");
|
|
944
|
+
});
|
|
945
|
+
it("should work with multiple derived fields", () => {
|
|
946
|
+
const products = schema({
|
|
947
|
+
_tableName: "products",
|
|
948
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
949
|
+
price: s.sqlite({ type: "int" }).clientInput({ value: 100 }),
|
|
950
|
+
quantity: s.sqlite({ type: "int" }).clientInput({ value: 5 }),
|
|
951
|
+
total: s.clientInput(0),
|
|
952
|
+
formattedPrice: s.clientInput(""),
|
|
953
|
+
}).derive({
|
|
954
|
+
forClient: {
|
|
955
|
+
total: (row) => row.price * row.quantity,
|
|
956
|
+
formattedPrice: (row) => `$${row.price}`,
|
|
957
|
+
},
|
|
958
|
+
});
|
|
959
|
+
const box = createSchemaBox({ products }, { products: {} });
|
|
960
|
+
const defaults = box.products.defaults;
|
|
961
|
+
expect(defaults.total).toBe(500);
|
|
962
|
+
expect(defaults.formattedPrice).toBe("$100");
|
|
963
|
+
});
|
|
964
|
+
it("should track dependencies for forDb derived fields", () => {
|
|
965
|
+
const contacts = schema({
|
|
966
|
+
_tableName: "contacts",
|
|
967
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
968
|
+
firstName: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
969
|
+
lastName: s.sqlite({ type: "varchar" }).clientInput({ value: "Doe" }),
|
|
970
|
+
searchName: s.sqlite({ type: "varchar", sqlOnly: true }),
|
|
971
|
+
}).derive({
|
|
972
|
+
forDb: {
|
|
973
|
+
searchName: (row) => `${row.firstName} ${row.lastName}`.trim().toLowerCase(),
|
|
974
|
+
},
|
|
975
|
+
});
|
|
976
|
+
const contactBox = createSchemaBox({ contacts }, { contacts: {} });
|
|
977
|
+
expect(contactBox.contacts.deriveDependencies.searchName).toEqual([
|
|
978
|
+
"firstName",
|
|
979
|
+
"lastName",
|
|
980
|
+
]);
|
|
981
|
+
expect(contactBox.contacts.transforms.parseForDb({
|
|
982
|
+
id: 1,
|
|
983
|
+
firstName: "Ada",
|
|
984
|
+
lastName: "Lovelace",
|
|
985
|
+
})).toMatchObject({ searchName: "ada lovelace" });
|
|
986
|
+
});
|
|
987
|
+
it("should allow forDb derived fields on visible DB columns", () => {
|
|
988
|
+
const contacts = schema({
|
|
989
|
+
_tableName: "contacts",
|
|
990
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
991
|
+
firstName: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
992
|
+
lastName: s.sqlite({ type: "varchar" }).clientInput({ value: "Doe" }),
|
|
993
|
+
fullName: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
994
|
+
}).derive({
|
|
995
|
+
forDb: {
|
|
996
|
+
fullName: (row) => `${row.firstName} ${row.lastName}`.trim(),
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
const contactBox = createSchemaBox({ contacts }, { contacts: {} });
|
|
1000
|
+
expect(contactBox.contacts.transforms.parseForDb({
|
|
1001
|
+
id: 1,
|
|
1002
|
+
firstName: "Ada",
|
|
1003
|
+
lastName: "Lovelace",
|
|
1004
|
+
fullName: "stale",
|
|
1005
|
+
})).toMatchObject({ fullName: "Ada Lovelace" });
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
describe("refine", () => {
|
|
1009
|
+
it("should run server refinement on parseForDb", () => {
|
|
1010
|
+
const events = schema({
|
|
1011
|
+
_tableName: "events",
|
|
1012
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
1013
|
+
startDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
1014
|
+
endDate: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
1015
|
+
content: s.sqlite({ type: "varchar", nullable: true }).clientInput({
|
|
1016
|
+
value: null,
|
|
1017
|
+
schema: z.string().nullable(),
|
|
1018
|
+
}),
|
|
1019
|
+
isPublished: s.sqlite({ type: "boolean" }).clientInput({ value: false }),
|
|
1020
|
+
}).refine({
|
|
1021
|
+
server: (row) => {
|
|
1022
|
+
const errors = [];
|
|
1023
|
+
if (row.startDate && row.endDate && row.startDate > row.endDate) {
|
|
1024
|
+
errors.push({
|
|
1025
|
+
path: ["endDate"],
|
|
1026
|
+
message: "End date must be after start date",
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
if (row.isPublished && !row.content) {
|
|
1030
|
+
errors.push({
|
|
1031
|
+
path: ["content"],
|
|
1032
|
+
message: "Published events must have content",
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
return errors.length > 0 ? errors : undefined;
|
|
1036
|
+
},
|
|
1037
|
+
});
|
|
1038
|
+
const box = createSchemaBox({ events }, { events: {} });
|
|
1039
|
+
expect(() => box.events.transforms.parseForDb({
|
|
1040
|
+
id: 1,
|
|
1041
|
+
startDate: "2024-12-31",
|
|
1042
|
+
endDate: "2024-01-01",
|
|
1043
|
+
content: null,
|
|
1044
|
+
isPublished: false,
|
|
1045
|
+
})).toThrow("End date must be after start date");
|
|
1046
|
+
expect(() => box.events.transforms.parseForDb({
|
|
1047
|
+
id: 1,
|
|
1048
|
+
startDate: "2024-01-01",
|
|
1049
|
+
endDate: "2024-12-31",
|
|
1050
|
+
content: null,
|
|
1051
|
+
isPublished: true,
|
|
1052
|
+
})).toThrow("Published events must have content");
|
|
1053
|
+
const valid = box.events.transforms.parseForDb({
|
|
1054
|
+
id: 1,
|
|
1055
|
+
startDate: "2024-01-01",
|
|
1056
|
+
endDate: "2024-12-31",
|
|
1057
|
+
content: "Event details",
|
|
1058
|
+
isPublished: true,
|
|
1059
|
+
});
|
|
1060
|
+
expect(valid).toMatchObject({
|
|
1061
|
+
startDate: "2024-01-01",
|
|
1062
|
+
endDate: "2024-12-31",
|
|
1063
|
+
content: "Event details",
|
|
1064
|
+
isPublished: true,
|
|
1065
|
+
});
|
|
1066
|
+
});
|
|
1067
|
+
it("should run client refinement on client input", () => {
|
|
1068
|
+
const forms = schema({
|
|
1069
|
+
_tableName: "forms",
|
|
1070
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
1071
|
+
password: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
1072
|
+
confirmPassword: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
1073
|
+
}).refine({
|
|
1074
|
+
client: (row) => {
|
|
1075
|
+
if (row.password !== row.confirmPassword) {
|
|
1076
|
+
return {
|
|
1077
|
+
path: ["confirmPassword"],
|
|
1078
|
+
message: "Passwords must match",
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
return undefined;
|
|
1082
|
+
},
|
|
1083
|
+
});
|
|
1084
|
+
const box = createSchemaBox({ forms }, { forms: {} });
|
|
1085
|
+
const result = box.forms.schemas.clientInput.safeParse({
|
|
1086
|
+
id: 1,
|
|
1087
|
+
password: "secret",
|
|
1088
|
+
confirmPassword: "different",
|
|
1089
|
+
});
|
|
1090
|
+
expect(result.success).toBe(false);
|
|
1091
|
+
if (!result.success) {
|
|
1092
|
+
expect(result.error.issues[0].message).toBe("Passwords must match");
|
|
1093
|
+
}
|
|
1094
|
+
const validResult = box.forms.schemas.clientInput.safeParse({
|
|
1095
|
+
id: 1,
|
|
1096
|
+
password: "secret",
|
|
1097
|
+
confirmPassword: "secret",
|
|
1098
|
+
});
|
|
1099
|
+
expect(validResult.success).toBe(true);
|
|
1100
|
+
});
|
|
1101
|
+
it("should track refinement dependencies", () => {
|
|
1102
|
+
const items = schema({
|
|
1103
|
+
_tableName: "items",
|
|
1104
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
1105
|
+
min: s.sqlite({ type: "int" }).clientInput({ value: 0 }),
|
|
1106
|
+
max: s.sqlite({ type: "int" }).clientInput({ value: 100 }),
|
|
1107
|
+
}).refine({
|
|
1108
|
+
server: (row) => {
|
|
1109
|
+
if (row.min > row.max) {
|
|
1110
|
+
return { path: ["max"], message: "Max must be >= min" };
|
|
1111
|
+
}
|
|
1112
|
+
return undefined;
|
|
1113
|
+
},
|
|
1114
|
+
});
|
|
1115
|
+
const box = createSchemaBox({ items }, { items: {} });
|
|
1116
|
+
expect(box.items.refineDependencies.server).toEqual(["min", "max"]);
|
|
1117
|
+
expect(box.items.refineDependencies.client).toEqual([]);
|
|
1118
|
+
});
|
|
1119
|
+
it("should support chaining derive and refine", () => {
|
|
1120
|
+
const records = schema({
|
|
1121
|
+
_tableName: "records",
|
|
1122
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
1123
|
+
firstName: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
1124
|
+
lastName: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
1125
|
+
fullName: s.sqlite({ type: "varchar", sqlOnly: true }),
|
|
1126
|
+
})
|
|
1127
|
+
.derive({
|
|
1128
|
+
forDb: {
|
|
1129
|
+
fullName: (row) => `${row.firstName} ${row.lastName}`.trim(),
|
|
1130
|
+
},
|
|
1131
|
+
})
|
|
1132
|
+
.refine({
|
|
1133
|
+
server: (row) => {
|
|
1134
|
+
if (!row.firstName && !row.lastName) {
|
|
1135
|
+
return { path: ["firstName"], message: "Name is required" };
|
|
1136
|
+
}
|
|
1137
|
+
return undefined;
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
const box = createSchemaBox({ records }, { records: {} });
|
|
1141
|
+
expect(box.records.deriveDependencies.fullName).toEqual([
|
|
1142
|
+
"firstName",
|
|
1143
|
+
"lastName",
|
|
1144
|
+
]);
|
|
1145
|
+
expect(box.records.refineDependencies.server).toEqual([
|
|
1146
|
+
"firstName",
|
|
1147
|
+
"lastName",
|
|
1148
|
+
]);
|
|
1149
|
+
const result = box.records.transforms.parseForDb({
|
|
1150
|
+
id: 1,
|
|
1151
|
+
firstName: "John",
|
|
1152
|
+
lastName: "Doe",
|
|
1153
|
+
});
|
|
1154
|
+
expect(result).toMatchObject({ fullName: "John Doe" });
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
describe("client-only fields", () => {
|
|
1158
|
+
it("should exclude client-only fields from database transforms", () => {
|
|
1159
|
+
const tasks = schema({
|
|
1160
|
+
_tableName: "tasks",
|
|
1161
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
1162
|
+
title: s.sqlite({ type: "varchar" }).clientInput({ value: "" }),
|
|
1163
|
+
statusLabel: s.clientInput(""),
|
|
1164
|
+
});
|
|
1165
|
+
const taskBox = createSchemaBox({ tasks }, { tasks: {} });
|
|
1166
|
+
expect(taskBox.tasks.transforms.parseForDb({
|
|
1167
|
+
id: 1,
|
|
1168
|
+
title: "Ship it",
|
|
1169
|
+
statusLabel: "Local only",
|
|
1170
|
+
})).toEqual({ id: 1, title: "Ship it" });
|
|
1171
|
+
expect(taskBox.tasks.transforms.parsePatchForDb({
|
|
1172
|
+
statusLabel: "Still local only",
|
|
1173
|
+
})).toEqual({});
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
describe("sqlOnly with derive in relations", () => {
|
|
1177
|
+
const users = schema({
|
|
1178
|
+
_tableName: "users",
|
|
1179
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
1180
|
+
value: () => "user-123",
|
|
1181
|
+
schema: z.string(),
|
|
1182
|
+
}),
|
|
1183
|
+
internalScore: s.sqlite({ type: "int", sqlOnly: true }),
|
|
1184
|
+
posts: s.hasMany({ count: 1 }),
|
|
1185
|
+
});
|
|
1186
|
+
const posts = schema({
|
|
1187
|
+
_tableName: "posts",
|
|
1188
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
1189
|
+
title: s.sqlite({ type: "varchar" }).clientInput({ value: "Post" }),
|
|
1190
|
+
authorId: s.reference(() => users.id),
|
|
1191
|
+
preview: s.clientInput(""),
|
|
1192
|
+
}).derive({
|
|
1193
|
+
forClient: {
|
|
1194
|
+
preview: (row) => row.title.substring(0, 5),
|
|
1195
|
+
},
|
|
1196
|
+
});
|
|
1197
|
+
const box = createSchemaBox({ users, posts }, {
|
|
1198
|
+
users: { posts: { fromKey: "id", toKey: posts.authorId } },
|
|
1199
|
+
});
|
|
1200
|
+
it("should exclude sqlOnly fields from view but include derived", () => {
|
|
1201
|
+
const view = box.users.createView({ posts: true });
|
|
1202
|
+
const viewClientKeys = Object.keys(view.schemas.client.shape);
|
|
1203
|
+
expect(viewClientKeys).not.toContain("internalScore");
|
|
1204
|
+
expect(viewClientKeys).toContain("id");
|
|
1205
|
+
expect(viewClientKeys).toContain("posts");
|
|
1206
|
+
});
|
|
1207
|
+
it("should include derived fields in view", () => {
|
|
1208
|
+
const view = box.posts.createView({});
|
|
1209
|
+
const defaults = view.defaults();
|
|
1210
|
+
expect(defaults.preview).toBe("Post");
|
|
1211
|
+
});
|
|
1212
|
+
it("should apply toClient transforms correctly with derived", () => {
|
|
1213
|
+
const view = box.users.createView({ posts: true });
|
|
1214
|
+
const { toClient } = view.transforms;
|
|
1215
|
+
const dbData = {
|
|
1216
|
+
id: 1,
|
|
1217
|
+
internalScore: 99,
|
|
1218
|
+
posts: [{ id: 10, title: "Hello World", authorId: 1 }],
|
|
1219
|
+
};
|
|
1220
|
+
const result = toClient(dbData);
|
|
1221
|
+
expect(result.internalScore).toBeUndefined();
|
|
1222
|
+
expect(result.posts[0].preview).toBe("Hello");
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
describe("defaultsDefinition", () => {
|
|
1226
|
+
const users = schema({
|
|
1227
|
+
_tableName: "users",
|
|
1228
|
+
id: s
|
|
1229
|
+
.sqlite({ type: "int", pk: true })
|
|
1230
|
+
.clientInput({ value: "user-123", schema: z.string() }),
|
|
1231
|
+
name: s.sqlite({ type: "varchar" }).clientInput({ value: "John" }),
|
|
1232
|
+
posts: s.hasMany({ count: 2 }),
|
|
1233
|
+
profile: s.hasOne(true),
|
|
1234
|
+
});
|
|
1235
|
+
const posts = schema({
|
|
1236
|
+
_tableName: "posts",
|
|
1237
|
+
id: s.sqlite({ type: "int", pk: true }),
|
|
1238
|
+
title: s.sqlite({ type: "varchar" }).clientInput({ value: "Default Post" }),
|
|
1239
|
+
authorId: s.reference(() => users.id),
|
|
1240
|
+
user: s.hasOne(true),
|
|
1241
|
+
});
|
|
1242
|
+
const box = createSchemaBox({ users, posts }, {
|
|
1243
|
+
users: { posts: { fromKey: "id", toKey: posts.authorId } },
|
|
1244
|
+
posts: { user: { fromKey: "id", toKey: users.id } },
|
|
1245
|
+
});
|
|
1246
|
+
it("should have defaultsDefinition on schema box", () => {
|
|
1247
|
+
const def = box.users.defaultsDefinition;
|
|
1248
|
+
expect(def).toBeDefined();
|
|
1249
|
+
expect(def.id).toBe("user-123");
|
|
1250
|
+
expect(def.name).toBe("John");
|
|
1251
|
+
// Check array relation logic
|
|
1252
|
+
expect(def.posts).toBeInstanceOf(Array);
|
|
1253
|
+
expect(def.posts).toHaveLength(2);
|
|
1254
|
+
expect(def.posts[0].title).toBe("Default Post");
|
|
1255
|
+
// Check __def__ single-element logic
|
|
1256
|
+
expect(def.__def__posts).toBeDefined();
|
|
1257
|
+
expect(Array.isArray(def.__def__posts)).toBe(false);
|
|
1258
|
+
expect(def.__def__posts.title).toBe("Default Post");
|
|
1259
|
+
});
|
|
1260
|
+
it("should have defaultsDefinition on views", () => {
|
|
1261
|
+
const view = box.users.createView({
|
|
1262
|
+
posts: { user: true },
|
|
1263
|
+
});
|
|
1264
|
+
const def = view.defaultsDefinition();
|
|
1265
|
+
expect(def).toBeDefined();
|
|
1266
|
+
expect(def.posts).toBeInstanceOf(Array);
|
|
1267
|
+
expect(def.posts).toHaveLength(2);
|
|
1268
|
+
// Nested relation array test
|
|
1269
|
+
expect(def.posts[0].user).toBeDefined();
|
|
1270
|
+
expect(def.posts[0].user?.name).toBe("John");
|
|
1271
|
+
// Nested __def__ single-element logic
|
|
1272
|
+
expect(def.__def__posts).toBeDefined();
|
|
1273
|
+
expect(Array.isArray(def.__def__posts)).toBe(false);
|
|
1274
|
+
expect(def.__def__posts.title).toBe("Default Post");
|
|
1275
|
+
expect(def.__def__posts.user).toBeDefined();
|
|
1276
|
+
expect(def.__def__posts.user?.name).toBe("John");
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
describe("dynamic value functions re-run on each view.defaults() call", () => {
|
|
1280
|
+
const factory = schema({
|
|
1281
|
+
_tableName: "factories",
|
|
1282
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
1283
|
+
value: () => `temp_${Math.random().toString(36).substr(2, 8)}`,
|
|
1284
|
+
schema: z.string(),
|
|
1285
|
+
clientPk: true,
|
|
1286
|
+
}),
|
|
1287
|
+
name: s
|
|
1288
|
+
.sqlite({ type: "varchar", length: 100 })
|
|
1289
|
+
.clientInput({ value: "MyFactory" }),
|
|
1290
|
+
boxes: s.hasMany({ count: 2 }),
|
|
1291
|
+
});
|
|
1292
|
+
const box = schema({
|
|
1293
|
+
_tableName: "boxes",
|
|
1294
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
1295
|
+
value: () => `box_${Math.random().toString(36).substr(2, 8)}`,
|
|
1296
|
+
schema: z.string(),
|
|
1297
|
+
clientPk: true,
|
|
1298
|
+
}),
|
|
1299
|
+
factoryId: s.reference(() => factory.id),
|
|
1300
|
+
variant: s.hasOne(true),
|
|
1301
|
+
});
|
|
1302
|
+
const boxVariant = schema({
|
|
1303
|
+
_tableName: "box_variants",
|
|
1304
|
+
id: s.sqlite({ type: "int", pk: true }).clientInput({
|
|
1305
|
+
value: () => `var_${Math.random().toString(36).substr(2, 8)}`,
|
|
1306
|
+
schema: z.string(),
|
|
1307
|
+
clientPk: true,
|
|
1308
|
+
}),
|
|
1309
|
+
boxId: s.reference(() => box.id),
|
|
1310
|
+
label: s
|
|
1311
|
+
.sqlite({ type: "varchar", length: 50 })
|
|
1312
|
+
.clientInput({ value: "Standard" }),
|
|
1313
|
+
});
|
|
1314
|
+
const box_ = createSchemaBox({ factory, box, boxVariant }, {
|
|
1315
|
+
factory: {
|
|
1316
|
+
boxes: { fromKey: "id", toKey: box.factoryId },
|
|
1317
|
+
},
|
|
1318
|
+
box: {
|
|
1319
|
+
variant: { fromKey: "id", toKey: boxVariant.boxId },
|
|
1320
|
+
},
|
|
1321
|
+
boxVariant: {},
|
|
1322
|
+
});
|
|
1323
|
+
it("should generate unique top-level defaults on each call", () => {
|
|
1324
|
+
const view = box_.factory.createView({ boxes: { variant: true } });
|
|
1325
|
+
const first = view.defaults();
|
|
1326
|
+
const second = view.defaults();
|
|
1327
|
+
expect(first.id).not.toBe(second.id);
|
|
1328
|
+
expect(first.name).toBe("MyFactory");
|
|
1329
|
+
});
|
|
1330
|
+
it("should generate unique nested relation defaults on each call", () => {
|
|
1331
|
+
const view = box_.factory.createView({ boxes: { variant: true } });
|
|
1332
|
+
const first = view.defaults();
|
|
1333
|
+
const second = view.defaults();
|
|
1334
|
+
expect(first.boxes).toHaveLength(2);
|
|
1335
|
+
expect(second.boxes).toHaveLength(2);
|
|
1336
|
+
first.boxes.forEach((b, i) => {
|
|
1337
|
+
expect(b.id).not.toBe(second.boxes[i].id);
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
it("should generate unique deeply nested defaults on each call", () => {
|
|
1341
|
+
const view = box_.factory.createView({ boxes: { variant: true } });
|
|
1342
|
+
const first = view.defaults();
|
|
1343
|
+
const second = view.defaults();
|
|
1344
|
+
first.boxes.forEach((b, i) => {
|
|
1345
|
+
const firstVariant = b.variant;
|
|
1346
|
+
const secondVariant = second.boxes[i].variant;
|
|
1347
|
+
expect(firstVariant.id).not.toBe(secondVariant.id);
|
|
1348
|
+
});
|
|
1349
|
+
});
|
|
1350
|
+
it("should generate unique values within the same defaults call", () => {
|
|
1351
|
+
const view = box_.factory.createView({ boxes: { variant: true } });
|
|
1352
|
+
const defaults = view.defaults();
|
|
1353
|
+
const ids = new Set([
|
|
1354
|
+
defaults.id,
|
|
1355
|
+
...(defaults.boxes?.map((b) => b.id) ?? []),
|
|
1356
|
+
...(defaults.boxes?.flatMap((b) => b.variant ? [b.variant.id] : []) ?? []),
|
|
1357
|
+
]);
|
|
1358
|
+
expect(ids.size).toBe(5);
|
|
1359
|
+
});
|
|
1360
|
+
it("should generate unique values across defaultsDefinition calls", () => {
|
|
1361
|
+
const view = box_.factory.createView({ boxes: { variant: true } });
|
|
1362
|
+
const first = view.defaultsDefinition();
|
|
1363
|
+
const second = view.defaultsDefinition();
|
|
1364
|
+
expect(first.id).not.toBe(second.id);
|
|
1365
|
+
expect(first.boxes[0].variant.id).not.toBe(second.boxes[0].variant.id);
|
|
1366
|
+
});
|
|
1367
|
+
});
|