@lunora/seed 0.0.0 → 1.0.0-alpha.2
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/LICENSE.md +105 -0
- package/README.md +170 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.mjs +2 -0
- package/dist/packem_shared/createSeedClient-CI9LRLTi.mjs +92 -0
- package/dist/packem_shared/plan-DirmkctQ.mjs +497 -0
- package/dist/packem_shared/plan.d-BQ6LiWIk.d.mts +78 -0
- package/dist/packem_shared/plan.d-BQ6LiWIk.d.ts +78 -0
- package/dist/packem_shared/seedPlan-CDSmSgjY.mjs +1 -0
- package/dist/testing.d.mts +5 -0
- package/dist/testing.d.ts +5 -0
- package/dist/testing.mjs +53 -0
- package/package.json +45 -14
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import { optionalInner } from '@lunora/values';
|
|
3
|
+
|
|
4
|
+
let hashSalt = 0;
|
|
5
|
+
const compareStrings = (a, b) => {
|
|
6
|
+
if (a < b) {
|
|
7
|
+
return -1;
|
|
8
|
+
}
|
|
9
|
+
if (a > b) {
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
return 0;
|
|
13
|
+
};
|
|
14
|
+
const stableStringify = (input) => {
|
|
15
|
+
if (input === void 0) {
|
|
16
|
+
return "undefined";
|
|
17
|
+
}
|
|
18
|
+
if (typeof input === "bigint") {
|
|
19
|
+
return `${input.toString()}n`;
|
|
20
|
+
}
|
|
21
|
+
if (input === null || typeof input !== "object") {
|
|
22
|
+
return JSON.stringify(input) ?? "null";
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(input)) {
|
|
25
|
+
return `[${input.map((item) => stableStringify(item)).join(",")}]`;
|
|
26
|
+
}
|
|
27
|
+
const entries = Object.keys(input).toSorted(compareStrings).map((key) => `${JSON.stringify(key)}:${stableStringify(input[key])}`);
|
|
28
|
+
return `{${entries.join(",")}}`;
|
|
29
|
+
};
|
|
30
|
+
const cyrb53 = (text, seed) => {
|
|
31
|
+
let h1 = 3735928559 ^ seed;
|
|
32
|
+
let h2 = 1103547991 ^ seed;
|
|
33
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
34
|
+
const ch = text.codePointAt(index) ?? 0;
|
|
35
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
36
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
37
|
+
}
|
|
38
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
39
|
+
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
40
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
41
|
+
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
42
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
43
|
+
};
|
|
44
|
+
const STRING_DOMAIN_TAG = "\0";
|
|
45
|
+
const hashInput = (input) => {
|
|
46
|
+
const text = typeof input === "string" ? STRING_DOMAIN_TAG + input : stableStringify(input);
|
|
47
|
+
return cyrb53(text, hashSalt) % 4294967296;
|
|
48
|
+
};
|
|
49
|
+
const setHashKey = (key) => {
|
|
50
|
+
if (typeof key === "number") {
|
|
51
|
+
hashSalt = key >>> 0;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (typeof key === "string") {
|
|
55
|
+
hashSalt = cyrb53(key, 0) % 4294967296;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let folded = 0;
|
|
59
|
+
for (const word of key) {
|
|
60
|
+
folded = Math.imul(folded ^ word, 2654435761) >>> 0;
|
|
61
|
+
}
|
|
62
|
+
hashSalt = folded;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const seeded = (input, produce) => {
|
|
66
|
+
faker.seed(hashInput(input));
|
|
67
|
+
return produce();
|
|
68
|
+
};
|
|
69
|
+
const resolveCount = (input, range) => {
|
|
70
|
+
if (typeof range === "number") {
|
|
71
|
+
return range;
|
|
72
|
+
}
|
|
73
|
+
const [min, max] = range;
|
|
74
|
+
return seeded(["__count__", input], () => faker.number.int({ max, min }));
|
|
75
|
+
};
|
|
76
|
+
const capitalizeWord = (word) => word.length === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1);
|
|
77
|
+
const LOWER_ALPHA = /[a-z]/;
|
|
78
|
+
const UPPER_ALPHA = /[A-Z]/;
|
|
79
|
+
const DIGIT = /\d/;
|
|
80
|
+
const copycat = {
|
|
81
|
+
bool(input) {
|
|
82
|
+
return seeded(input, () => faker.datatype.boolean());
|
|
83
|
+
},
|
|
84
|
+
char(input) {
|
|
85
|
+
return seeded(input, () => faker.string.alpha(1));
|
|
86
|
+
},
|
|
87
|
+
city(input) {
|
|
88
|
+
return seeded(input, () => faker.location.city());
|
|
89
|
+
},
|
|
90
|
+
country(input) {
|
|
91
|
+
return seeded(input, () => faker.location.country());
|
|
92
|
+
},
|
|
93
|
+
countryCode(input) {
|
|
94
|
+
return seeded(input, () => faker.location.countryCode());
|
|
95
|
+
},
|
|
96
|
+
/** ISO-8601 date string between `min`/`max` (default years 1980–2020). */
|
|
97
|
+
dateString(input, options) {
|
|
98
|
+
const min = options?.min ?? /* @__PURE__ */ new Date("1980-01-01T00:00:00.000Z");
|
|
99
|
+
const max = options?.max ?? /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
|
|
100
|
+
return seeded(input, () => faker.date.between({ from: min, to: max }).toISOString());
|
|
101
|
+
},
|
|
102
|
+
digit(input) {
|
|
103
|
+
return seeded(input, () => String(faker.number.int({ max: 9, min: 0 })));
|
|
104
|
+
},
|
|
105
|
+
email(input) {
|
|
106
|
+
return seeded(input, () => faker.internet.email().toLowerCase());
|
|
107
|
+
},
|
|
108
|
+
firstName(input) {
|
|
109
|
+
return seeded(input, () => faker.person.firstName());
|
|
110
|
+
},
|
|
111
|
+
float(input, options) {
|
|
112
|
+
const { fractionDigits = 2, max = 1e3, min = 0 } = options ?? {};
|
|
113
|
+
return seeded(input, () => faker.number.float({ fractionDigits, max, min }));
|
|
114
|
+
},
|
|
115
|
+
fullName(input) {
|
|
116
|
+
return seeded(input, () => faker.person.fullName());
|
|
117
|
+
},
|
|
118
|
+
hex(input) {
|
|
119
|
+
return seeded(input, () => faker.string.hexadecimal({ casing: "lower", length: 1, prefix: "" }));
|
|
120
|
+
},
|
|
121
|
+
int(input, options) {
|
|
122
|
+
const { max = 1e3, min = 0 } = options ?? {};
|
|
123
|
+
return seeded(input, () => faker.number.int({ max, min }));
|
|
124
|
+
},
|
|
125
|
+
ipv4(input) {
|
|
126
|
+
return seeded(input, () => faker.internet.ipv4());
|
|
127
|
+
},
|
|
128
|
+
lastName(input) {
|
|
129
|
+
return seeded(input, () => faker.person.lastName());
|
|
130
|
+
},
|
|
131
|
+
mac(input) {
|
|
132
|
+
return seeded(input, () => faker.internet.mac());
|
|
133
|
+
},
|
|
134
|
+
/** Pick the array element corresponding to `input`. Returns `undefined` for an empty array. */
|
|
135
|
+
oneOf(input, values) {
|
|
136
|
+
if (values.length === 0) {
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
return seeded(input, () => faker.helpers.arrayElement(values));
|
|
140
|
+
},
|
|
141
|
+
paragraph(input) {
|
|
142
|
+
return seeded(input, () => faker.lorem.paragraph());
|
|
143
|
+
},
|
|
144
|
+
password(input) {
|
|
145
|
+
return seeded(input, () => faker.internet.password());
|
|
146
|
+
},
|
|
147
|
+
phoneNumber(input) {
|
|
148
|
+
return seeded(input, () => faker.phone.number());
|
|
149
|
+
},
|
|
150
|
+
/**
|
|
151
|
+
* Scramble a string in place: letters become seeded letters (case
|
|
152
|
+
* preserved), digits become seeded digits, every other character is kept.
|
|
153
|
+
* Length is preserved. Characters listed in `preserve` pass through untouched.
|
|
154
|
+
*/
|
|
155
|
+
scramble(input, options) {
|
|
156
|
+
const preserve = new Set(options?.preserve);
|
|
157
|
+
return Array.from({ length: input.length }, (_unused, index) => {
|
|
158
|
+
const char = input.charAt(index);
|
|
159
|
+
if (preserve.has(char)) {
|
|
160
|
+
return char;
|
|
161
|
+
}
|
|
162
|
+
if (LOWER_ALPHA.test(char)) {
|
|
163
|
+
return seeded([input, index, "l"], () => faker.string.alpha({ casing: "lower", length: 1 }));
|
|
164
|
+
}
|
|
165
|
+
if (UPPER_ALPHA.test(char)) {
|
|
166
|
+
return seeded([input, index, "u"], () => faker.string.alpha({ casing: "upper", length: 1 }));
|
|
167
|
+
}
|
|
168
|
+
if (DIGIT.test(char)) {
|
|
169
|
+
return seeded([input, index, "d"], () => String(faker.number.int({ max: 9, min: 0 })));
|
|
170
|
+
}
|
|
171
|
+
return char;
|
|
172
|
+
}).join("");
|
|
173
|
+
},
|
|
174
|
+
sentence(input, options) {
|
|
175
|
+
return seeded(input, () => faker.lorem.sentence(options ? { max: options.max ?? 8, min: options.min ?? 3 } : void 0));
|
|
176
|
+
},
|
|
177
|
+
slug(input) {
|
|
178
|
+
return seeded(input, () => faker.lorem.slug());
|
|
179
|
+
},
|
|
180
|
+
/** Pick a deterministic subset (size within `range`) of `values`, no repeats. */
|
|
181
|
+
someOf(input, range, values) {
|
|
182
|
+
const count = Math.min(resolveCount(["__some__", input], range), values.length);
|
|
183
|
+
return seeded(input, () => faker.helpers.arrayElements(values, count));
|
|
184
|
+
},
|
|
185
|
+
streetAddress(input) {
|
|
186
|
+
return seeded(input, () => faker.location.streetAddress());
|
|
187
|
+
},
|
|
188
|
+
streetName(input) {
|
|
189
|
+
return seeded(input, () => faker.location.street());
|
|
190
|
+
},
|
|
191
|
+
timezone(input) {
|
|
192
|
+
return seeded(input, () => faker.location.timeZone());
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* Call `produce` once per element for a deterministic count within `range`,
|
|
196
|
+
* passing each a distinct sub-input so the elements differ but stay stable.
|
|
197
|
+
*/
|
|
198
|
+
times(input, range, produce) {
|
|
199
|
+
const count = resolveCount(input, range);
|
|
200
|
+
const out = [];
|
|
201
|
+
for (let index = 0; index < count; index += 1) {
|
|
202
|
+
out.push(produce([input, index]));
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
},
|
|
206
|
+
url(input) {
|
|
207
|
+
return seeded(input, () => faker.internet.url());
|
|
208
|
+
},
|
|
209
|
+
username(input) {
|
|
210
|
+
return seeded(input, () => faker.internet.username());
|
|
211
|
+
},
|
|
212
|
+
uuid(input) {
|
|
213
|
+
return seeded(input, () => faker.string.uuid());
|
|
214
|
+
},
|
|
215
|
+
word(input, options) {
|
|
216
|
+
const word = seeded(input, () => faker.lorem.word());
|
|
217
|
+
return options?.capitalize === true ? capitalizeWord(word) : word;
|
|
218
|
+
},
|
|
219
|
+
words(input, options) {
|
|
220
|
+
return seeded(input, () => faker.lorem.words(options ? { max: options.max ?? 5, min: options.min ?? 2 } : void 0));
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const metaOf = (validator) => validator._meta ?? {};
|
|
225
|
+
const unwrapOptional = (validator) => optionalInner(validator) ?? validator;
|
|
226
|
+
const hasServerDefault = (validator) => {
|
|
227
|
+
const { column } = metaOf(validator);
|
|
228
|
+
if (column?.defaultValue !== void 0 || column?.defaultFn !== void 0) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const inner = optionalInner(validator);
|
|
232
|
+
return inner === void 0 ? false : hasServerDefault(inner);
|
|
233
|
+
};
|
|
234
|
+
const describeField = (name, validator) => {
|
|
235
|
+
const inner = unwrapOptional(validator);
|
|
236
|
+
const meta = metaOf(inner);
|
|
237
|
+
return {
|
|
238
|
+
fkTable: inner.kind === "id" ? meta.tableName : void 0,
|
|
239
|
+
hasServerDefault: hasServerDefault(validator),
|
|
240
|
+
kind: inner.kind,
|
|
241
|
+
name,
|
|
242
|
+
// `notNull` is tri-state: `true` (default / `.notNull()`), `false`
|
|
243
|
+
// (`.nullable()`), or `undefined` (no column metadata). Only an explicit
|
|
244
|
+
// `false` means the user opted the column into SQL `NULL` — which is what
|
|
245
|
+
// `fkFallback` keys off to emit `null` for an unresolved nullable FK. This
|
|
246
|
+
// mirrors codegen's own `isNullable` (`column?.notNull === false`).
|
|
247
|
+
nullable: meta.column?.notNull === false,
|
|
248
|
+
optional: validator.kind === "optional",
|
|
249
|
+
validator: inner
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
const introspectSchema = (schema) => {
|
|
253
|
+
const { tables } = schema;
|
|
254
|
+
return Object.entries(tables).map(([name, table]) => {
|
|
255
|
+
return {
|
|
256
|
+
fields: Object.entries(table.shape).map(([fieldName, validator]) => describeField(fieldName, validator)),
|
|
257
|
+
name
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
const orderTables = (specs, selected) => {
|
|
262
|
+
const byName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
263
|
+
const parentsOf = (name) => {
|
|
264
|
+
const spec = byName.get(name);
|
|
265
|
+
if (spec === void 0) {
|
|
266
|
+
return /* @__PURE__ */ new Set();
|
|
267
|
+
}
|
|
268
|
+
const parents = /* @__PURE__ */ new Set();
|
|
269
|
+
for (const field of spec.fields) {
|
|
270
|
+
if (field.fkTable !== void 0 && field.fkTable !== name && selected.has(field.fkTable)) {
|
|
271
|
+
parents.add(field.fkTable);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return parents;
|
|
275
|
+
};
|
|
276
|
+
const ordered = [];
|
|
277
|
+
const placed = /* @__PURE__ */ new Set();
|
|
278
|
+
const pending = [...selected].filter((name) => byName.has(name));
|
|
279
|
+
while (pending.length > 0) {
|
|
280
|
+
const readyIndex = pending.findIndex((name2) => [...parentsOf(name2)].every((parent) => placed.has(parent)));
|
|
281
|
+
const index = readyIndex === -1 ? 0 : readyIndex;
|
|
282
|
+
const [name] = pending.splice(index, 1);
|
|
283
|
+
if (name === void 0) {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
ordered.push(name);
|
|
287
|
+
placed.add(name);
|
|
288
|
+
}
|
|
289
|
+
return ordered;
|
|
290
|
+
};
|
|
291
|
+
const fkParentClosure = (specs, roots) => {
|
|
292
|
+
const byName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
293
|
+
const result = new Set(roots);
|
|
294
|
+
const stack = [...result];
|
|
295
|
+
while (stack.length > 0) {
|
|
296
|
+
const name = stack.pop();
|
|
297
|
+
if (name === void 0) {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
const spec = byName.get(name);
|
|
301
|
+
if (spec === void 0) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
for (const field of spec.fields) {
|
|
305
|
+
if (field.fkTable !== void 0 && field.fkTable !== name && byName.has(field.fkTable) && !result.has(field.fkTable)) {
|
|
306
|
+
result.add(field.fkTable);
|
|
307
|
+
stack.push(field.fkTable);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const constraintsOf = (validator) => metaOf(validator).constraints ?? {};
|
|
315
|
+
const STRING_HEURISTICS = [
|
|
316
|
+
{ generate: (input) => copycat.email(input), keywords: ["email"] },
|
|
317
|
+
{ generate: (input) => copycat.firstName(input), keywords: ["firstname"] },
|
|
318
|
+
{ generate: (input) => copycat.lastName(input), keywords: ["lastname", "surname"] },
|
|
319
|
+
{ generate: (input) => copycat.username(input), keywords: ["username"] },
|
|
320
|
+
{ generate: (input) => copycat.fullName(input), keywords: ["name"] },
|
|
321
|
+
{ generate: (input) => copycat.sentence(input, { max: 5, min: 2 }), keywords: ["title"] },
|
|
322
|
+
{ generate: (input) => copycat.url(input), keywords: ["url", "link", "image", "avatar"] },
|
|
323
|
+
{ generate: (input) => copycat.phoneNumber(input), keywords: ["phone"] },
|
|
324
|
+
{ generate: (input) => copycat.paragraph(input), keywords: ["description", "bio", "body", "content", "text"] },
|
|
325
|
+
{ generate: (input) => copycat.slug(input), keywords: ["slug", "key", "code"] },
|
|
326
|
+
{ generate: (input) => copycat.city(input), keywords: ["city"] },
|
|
327
|
+
{ generate: (input) => copycat.country(input), keywords: ["country"] },
|
|
328
|
+
{ generate: (input) => copycat.streetAddress(input), keywords: ["address", "street"] },
|
|
329
|
+
{ generate: (input) => copycat.password(input), keywords: ["password", "secret", "token"] }
|
|
330
|
+
];
|
|
331
|
+
const generateString = (fieldName, input, constraints) => {
|
|
332
|
+
const lower = fieldName.toLowerCase();
|
|
333
|
+
const rule = STRING_HEURISTICS.find((entry) => entry.keywords.some((keyword) => lower.includes(keyword)));
|
|
334
|
+
const value = rule === void 0 ? copycat.word(input) : rule.generate(input);
|
|
335
|
+
const { maxLength, minLength } = constraints;
|
|
336
|
+
if (maxLength !== void 0 && minLength !== void 0 && minLength > maxLength) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Seed constraint error for field "${fieldName}": minLength (${String(minLength)}) > maxLength (${String(maxLength)}). Adjust the schema constraints.`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
const truncated = maxLength !== void 0 && value.length > maxLength ? value.slice(0, maxLength) : value;
|
|
342
|
+
if (minLength !== void 0 && truncated.length < minLength) {
|
|
343
|
+
return truncated.padEnd(minLength, truncated.length > 0 ? truncated : "x");
|
|
344
|
+
}
|
|
345
|
+
return truncated;
|
|
346
|
+
};
|
|
347
|
+
const generateValue = (validator, fieldName, input) => {
|
|
348
|
+
const inner = unwrapOptional(validator);
|
|
349
|
+
const constraints = constraintsOf(inner);
|
|
350
|
+
if (constraints.enum !== void 0 && constraints.enum.length > 0) {
|
|
351
|
+
return copycat.oneOf(input, constraints.enum);
|
|
352
|
+
}
|
|
353
|
+
switch (inner.kind) {
|
|
354
|
+
case "any": {
|
|
355
|
+
return copycat.word(input);
|
|
356
|
+
}
|
|
357
|
+
case "array": {
|
|
358
|
+
const element = metaOf(inner).inner;
|
|
359
|
+
if (element === void 0) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
return copycat.times(input, [1, 3], (itemInput) => generateValue(element, fieldName, itemInput));
|
|
363
|
+
}
|
|
364
|
+
case "bigint": {
|
|
365
|
+
return copycat.int(input, { max: 1e6, min: 0 });
|
|
366
|
+
}
|
|
367
|
+
case "boolean": {
|
|
368
|
+
return copycat.bool(input);
|
|
369
|
+
}
|
|
370
|
+
case "bytes": {
|
|
371
|
+
return Array.from({ length: 8 }, (_, index) => copycat.int([input, index], { max: 255, min: 0 }));
|
|
372
|
+
}
|
|
373
|
+
case "date":
|
|
374
|
+
case "timestamp": {
|
|
375
|
+
return new Date(copycat.dateString(input)).getTime();
|
|
376
|
+
}
|
|
377
|
+
case "id": {
|
|
378
|
+
return copycat.uuid(input);
|
|
379
|
+
}
|
|
380
|
+
case "literal": {
|
|
381
|
+
return metaOf(inner).value;
|
|
382
|
+
}
|
|
383
|
+
case "null": {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
case "number": {
|
|
387
|
+
return copycat.int(input, { max: constraints.maximum ?? 1e3, min: constraints.minimum ?? 0 });
|
|
388
|
+
}
|
|
389
|
+
case "object": {
|
|
390
|
+
const shape = metaOf(inner).shape ?? {};
|
|
391
|
+
return Object.fromEntries(Object.entries(shape).map(([key, child]) => [key, generateValue(child, key, [input, key])]));
|
|
392
|
+
}
|
|
393
|
+
case "record": {
|
|
394
|
+
const { valueValidator } = metaOf(inner);
|
|
395
|
+
const entries = copycat.times(input, [1, 3], (itemInput) => {
|
|
396
|
+
const key = copycat.word(["k", itemInput]);
|
|
397
|
+
const value = valueValidator === void 0 ? copycat.word(["v", itemInput]) : generateValue(valueValidator, fieldName, ["v", itemInput]);
|
|
398
|
+
return [key, value];
|
|
399
|
+
});
|
|
400
|
+
return Object.fromEntries(entries);
|
|
401
|
+
}
|
|
402
|
+
case "storage": {
|
|
403
|
+
return `seed/${copycat.uuid(input)}`;
|
|
404
|
+
}
|
|
405
|
+
case "string": {
|
|
406
|
+
return generateString(fieldName, input, constraints);
|
|
407
|
+
}
|
|
408
|
+
case "union": {
|
|
409
|
+
const members = metaOf(inner).members ?? [];
|
|
410
|
+
const chosen = copycat.oneOf(input, members);
|
|
411
|
+
return chosen === void 0 ? copycat.word(input) : generateValue(chosen, fieldName, [input, "u"]);
|
|
412
|
+
}
|
|
413
|
+
default: {
|
|
414
|
+
return copycat.word(input);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const resolveOverride = (override, context) => typeof override === "function" ? override(context) : override;
|
|
420
|
+
const fkFallback = (field, input) => {
|
|
421
|
+
if (field.optional) {
|
|
422
|
+
return void 0;
|
|
423
|
+
}
|
|
424
|
+
return field.nullable ? null : copycat.uuid(input);
|
|
425
|
+
};
|
|
426
|
+
const fkPool = (field, table, localIndex, idsByTable, existingIds) => {
|
|
427
|
+
const fkTable = field.fkTable;
|
|
428
|
+
const seeded = fkTable === table ? (idsByTable.get(table) ?? []).slice(0, localIndex) : idsByTable.get(fkTable) ?? [];
|
|
429
|
+
const existing = existingIds[fkTable] ?? [];
|
|
430
|
+
return existing.length === 0 ? seeded : [...seeded, ...existing];
|
|
431
|
+
};
|
|
432
|
+
const cellInput = (seed, table, index, column) => [seed, table, index, column];
|
|
433
|
+
const generateField = (field, table, input, localIndex, idsByTable, existingIds) => {
|
|
434
|
+
if (field.fkTable !== void 0) {
|
|
435
|
+
const pool = fkPool(field, table, localIndex, idsByTable, existingIds);
|
|
436
|
+
if (pool.length === 0) {
|
|
437
|
+
return fkFallback(field, input);
|
|
438
|
+
}
|
|
439
|
+
return copycat.oneOf(input, pool);
|
|
440
|
+
}
|
|
441
|
+
if (field.hasServerDefault) {
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
return generateValue(field.validator, field.name, input);
|
|
445
|
+
};
|
|
446
|
+
const seedPlan = (schema, options = {}) => {
|
|
447
|
+
const { counts = {}, defaultCount = 10, existingIds = {}, indexOffset = {}, only, overrides = {}, seed = 0 } = options;
|
|
448
|
+
setHashKey(seed);
|
|
449
|
+
const specs = introspectSchema(schema);
|
|
450
|
+
const requested = new Set(only ?? specs.map((spec) => spec.name));
|
|
451
|
+
const selected = new Set([...fkParentClosure(specs, requested)].filter((table) => requested.has(table) || (existingIds[table] ?? []).length === 0));
|
|
452
|
+
const order = orderTables(specs, selected);
|
|
453
|
+
const specByName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
454
|
+
const idsByTable = /* @__PURE__ */ new Map();
|
|
455
|
+
const storeRows = {};
|
|
456
|
+
const plan = [];
|
|
457
|
+
for (const table of order) {
|
|
458
|
+
const spec = specByName.get(table);
|
|
459
|
+
if (spec === void 0) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const tableOverrides = overrides[table] ?? {};
|
|
463
|
+
const count = counts[table] ?? defaultCount;
|
|
464
|
+
const offset = indexOffset[table] ?? 0;
|
|
465
|
+
const ids = [];
|
|
466
|
+
idsByTable.set(table, ids);
|
|
467
|
+
const rows = [];
|
|
468
|
+
storeRows[table] = rows;
|
|
469
|
+
for (let localIndex = 0; localIndex < count; localIndex += 1) {
|
|
470
|
+
const index = offset + localIndex;
|
|
471
|
+
const row = {};
|
|
472
|
+
const apply = (field, fallback) => {
|
|
473
|
+
if (Object.hasOwn(tableOverrides, field)) {
|
|
474
|
+
const overridden = resolveOverride(tableOverrides[field], { field, index, row, store: storeRows, table });
|
|
475
|
+
if (overridden !== void 0) {
|
|
476
|
+
row[field] = overridden;
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const value = fallback();
|
|
481
|
+
if (value !== void 0) {
|
|
482
|
+
row[field] = value;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
apply("_id", () => copycat.uuid(cellInput(seed, table, index, "_id")));
|
|
486
|
+
ids.push(row._id);
|
|
487
|
+
for (const field of spec.fields) {
|
|
488
|
+
apply(field.name, () => generateField(field, table, cellInput(seed, table, index, field.name), localIndex, idsByTable, existingIds));
|
|
489
|
+
}
|
|
490
|
+
rows.push(row);
|
|
491
|
+
}
|
|
492
|
+
plan.push({ rows, table });
|
|
493
|
+
}
|
|
494
|
+
return plan;
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
export { setHashKey as a, copycat as c, introspectSchema as i, seedPlan as s };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Schema } from '@lunora/server';
|
|
2
|
+
/**
|
|
3
|
+
* The pure, I/O-free core of seeding. {@link seedPlan} introspects a schema,
|
|
4
|
+
* generates rows table-by-table in foreign-key order, and resolves every
|
|
5
|
+
* `v.id("parent")` column to a real id of an already-generated parent row. The
|
|
6
|
+
* result feeds every adapter (test harness, CLI, studio) — none of them
|
|
7
|
+
* re-implement generation.
|
|
8
|
+
*
|
|
9
|
+
* Determinism: a `seed` value selects the global copycat mapping, and each value
|
|
10
|
+
* is hashed from `[seed, table, index, field]`, so the same `(schema, options)`
|
|
11
|
+
* always yields byte-identical rows.
|
|
12
|
+
*/
|
|
13
|
+
/** A row context handed to an override function. */
|
|
14
|
+
interface OverrideContext {
|
|
15
|
+
field: string;
|
|
16
|
+
/** The row's absolute index (the `indexOffset` base plus its position in this batch). */
|
|
17
|
+
index: number;
|
|
18
|
+
/** The row built so far (system `_id` first, then earlier fields). */
|
|
19
|
+
row: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* A live, read-only view of every table's rows generated so far this run,
|
|
22
|
+
* keyed by table name. Lets an override correlate across tables (e.g. copy a
|
|
23
|
+
* field from the parent row a foreign key points at). Rows for the current
|
|
24
|
+
* table accumulate as they are built, so only earlier rows are visible.
|
|
25
|
+
*/
|
|
26
|
+
store: Readonly<Record<string, ReadonlyArray<Record<string, unknown>>>>;
|
|
27
|
+
table: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Per-table, per-field overrides. Each override is a static value or a function
|
|
31
|
+
* of the row context; field `_id` overrides the generated primary key.
|
|
32
|
+
*/
|
|
33
|
+
type SeedOverrides = Record<string, Record<string, unknown>>;
|
|
34
|
+
/** Per-table row counts. */
|
|
35
|
+
type SeedCounts = Record<string, number>;
|
|
36
|
+
interface SeedOptions {
|
|
37
|
+
/** Rows per table; falls back to {@link SeedOptions.defaultCount} when a table is absent. */
|
|
38
|
+
counts?: SeedCounts;
|
|
39
|
+
/** Count used for any selected table not present in `counts` (default `10`). */
|
|
40
|
+
defaultCount?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Ids of rows that already exist in the target store, keyed by table. Foreign
|
|
43
|
+
* keys may resolve to these in addition to freshly-seeded parents, and a
|
|
44
|
+
* parent table fully covered here is not re-seeded when it is only pulled in
|
|
45
|
+
* as an FK dependency (it is when named explicitly in `only`).
|
|
46
|
+
*/
|
|
47
|
+
existingIds?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
48
|
+
/**
|
|
49
|
+
* Per-table absolute index base for generation. Defaults to `0`. A client
|
|
50
|
+
* seeding the same table across several calls passes the running total so
|
|
51
|
+
* each batch hashes from fresh indices and never collides ids with an
|
|
52
|
+
* earlier batch.
|
|
53
|
+
*/
|
|
54
|
+
indexOffset?: Readonly<Record<string, number>>;
|
|
55
|
+
/**
|
|
56
|
+
* Restrict seeding to these tables. Transitive `v.id(...)` parents are added
|
|
57
|
+
* automatically (unless already covered by `existingIds`) so child foreign
|
|
58
|
+
* keys resolve to real rows. The result is still ordered by FK dependency.
|
|
59
|
+
* Default: all tables.
|
|
60
|
+
*/
|
|
61
|
+
only?: ReadonlyArray<string>;
|
|
62
|
+
/** Static values or functions overriding generated columns. */
|
|
63
|
+
overrides?: SeedOverrides;
|
|
64
|
+
/** Deterministic mapping selector — same seed ⇒ same rows. Default `0`. */
|
|
65
|
+
seed?: number;
|
|
66
|
+
}
|
|
67
|
+
/** One table's generated rows, in insert order. Each row carries an explicit `_id`. */
|
|
68
|
+
interface TablePlan {
|
|
69
|
+
rows: ReadonlyArray<Record<string, unknown>>;
|
|
70
|
+
table: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a deterministic, FK-consistent set of rows for `schema`.
|
|
74
|
+
* @returns one {@link TablePlan} per seeded table, ordered so a table's FK
|
|
75
|
+
* parents come before it.
|
|
76
|
+
*/
|
|
77
|
+
declare const seedPlan: (schema: Schema, options?: SeedOptions) => ReadonlyArray<TablePlan>;
|
|
78
|
+
export { OverrideContext as O, SeedCounts as S, TablePlan as T, SeedOptions as a, SeedOverrides as b, seedPlan as s };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Schema } from '@lunora/server';
|
|
2
|
+
/**
|
|
3
|
+
* The pure, I/O-free core of seeding. {@link seedPlan} introspects a schema,
|
|
4
|
+
* generates rows table-by-table in foreign-key order, and resolves every
|
|
5
|
+
* `v.id("parent")` column to a real id of an already-generated parent row. The
|
|
6
|
+
* result feeds every adapter (test harness, CLI, studio) — none of them
|
|
7
|
+
* re-implement generation.
|
|
8
|
+
*
|
|
9
|
+
* Determinism: a `seed` value selects the global copycat mapping, and each value
|
|
10
|
+
* is hashed from `[seed, table, index, field]`, so the same `(schema, options)`
|
|
11
|
+
* always yields byte-identical rows.
|
|
12
|
+
*/
|
|
13
|
+
/** A row context handed to an override function. */
|
|
14
|
+
interface OverrideContext {
|
|
15
|
+
field: string;
|
|
16
|
+
/** The row's absolute index (the `indexOffset` base plus its position in this batch). */
|
|
17
|
+
index: number;
|
|
18
|
+
/** The row built so far (system `_id` first, then earlier fields). */
|
|
19
|
+
row: Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* A live, read-only view of every table's rows generated so far this run,
|
|
22
|
+
* keyed by table name. Lets an override correlate across tables (e.g. copy a
|
|
23
|
+
* field from the parent row a foreign key points at). Rows for the current
|
|
24
|
+
* table accumulate as they are built, so only earlier rows are visible.
|
|
25
|
+
*/
|
|
26
|
+
store: Readonly<Record<string, ReadonlyArray<Record<string, unknown>>>>;
|
|
27
|
+
table: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Per-table, per-field overrides. Each override is a static value or a function
|
|
31
|
+
* of the row context; field `_id` overrides the generated primary key.
|
|
32
|
+
*/
|
|
33
|
+
type SeedOverrides = Record<string, Record<string, unknown>>;
|
|
34
|
+
/** Per-table row counts. */
|
|
35
|
+
type SeedCounts = Record<string, number>;
|
|
36
|
+
interface SeedOptions {
|
|
37
|
+
/** Rows per table; falls back to {@link SeedOptions.defaultCount} when a table is absent. */
|
|
38
|
+
counts?: SeedCounts;
|
|
39
|
+
/** Count used for any selected table not present in `counts` (default `10`). */
|
|
40
|
+
defaultCount?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Ids of rows that already exist in the target store, keyed by table. Foreign
|
|
43
|
+
* keys may resolve to these in addition to freshly-seeded parents, and a
|
|
44
|
+
* parent table fully covered here is not re-seeded when it is only pulled in
|
|
45
|
+
* as an FK dependency (it is when named explicitly in `only`).
|
|
46
|
+
*/
|
|
47
|
+
existingIds?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
48
|
+
/**
|
|
49
|
+
* Per-table absolute index base for generation. Defaults to `0`. A client
|
|
50
|
+
* seeding the same table across several calls passes the running total so
|
|
51
|
+
* each batch hashes from fresh indices and never collides ids with an
|
|
52
|
+
* earlier batch.
|
|
53
|
+
*/
|
|
54
|
+
indexOffset?: Readonly<Record<string, number>>;
|
|
55
|
+
/**
|
|
56
|
+
* Restrict seeding to these tables. Transitive `v.id(...)` parents are added
|
|
57
|
+
* automatically (unless already covered by `existingIds`) so child foreign
|
|
58
|
+
* keys resolve to real rows. The result is still ordered by FK dependency.
|
|
59
|
+
* Default: all tables.
|
|
60
|
+
*/
|
|
61
|
+
only?: ReadonlyArray<string>;
|
|
62
|
+
/** Static values or functions overriding generated columns. */
|
|
63
|
+
overrides?: SeedOverrides;
|
|
64
|
+
/** Deterministic mapping selector — same seed ⇒ same rows. Default `0`. */
|
|
65
|
+
seed?: number;
|
|
66
|
+
}
|
|
67
|
+
/** One table's generated rows, in insert order. Each row carries an explicit `_id`. */
|
|
68
|
+
interface TablePlan {
|
|
69
|
+
rows: ReadonlyArray<Record<string, unknown>>;
|
|
70
|
+
table: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a deterministic, FK-consistent set of rows for `schema`.
|
|
74
|
+
* @returns one {@link TablePlan} per seeded table, ordered so a table's FK
|
|
75
|
+
* parents come before it.
|
|
76
|
+
*/
|
|
77
|
+
declare const seedPlan: (schema: Schema, options?: SeedOptions) => ReadonlyArray<TablePlan>;
|
|
78
|
+
export { OverrideContext as O, SeedCounts as S, TablePlan as T, SeedOptions as a, SeedOverrides as b, seedPlan as s };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { s as seedPlan } from './plan-DirmkctQ.mjs';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Schema } from '@lunora/server';
|
|
2
|
+
import { TestHarness } from '@lunora/testing';
|
|
3
|
+
import { a as SeedOptions } from "./packem_shared/plan.d-BQ6LiWIk.mjs";
|
|
4
|
+
declare const seed: (harness: TestHarness, schema: Schema, options?: SeedOptions) => Promise<Record<string, string[]>>;
|
|
5
|
+
export { seed };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Schema } from '@lunora/server';
|
|
2
|
+
import { TestHarness } from '@lunora/testing';
|
|
3
|
+
import { a as SeedOptions } from "./packem_shared/plan.d-BQ6LiWIk.js";
|
|
4
|
+
declare const seed: (harness: TestHarness, schema: Schema, options?: SeedOptions) => Promise<Record<string, string[]>>;
|
|
5
|
+
export { seed };
|