@lunora/seed 0.0.0 → 1.0.0-alpha.1
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
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Schema } from '@lunora/server';
|
|
2
|
+
import { O as OverrideContext } from "./packem_shared/plan.d-BQ6LiWIk.mjs";
|
|
3
|
+
export { type S as SeedCounts, type a as SeedOptions, type b as SeedOverrides, type T as TablePlan, s as seedPlan } from "./packem_shared/plan.d-BQ6LiWIk.mjs";
|
|
4
|
+
/**
|
|
5
|
+
* A typed, schema-aware seed client — the ergonomic DX layer over {@link seedPlan}.
|
|
6
|
+
*
|
|
7
|
+
* Where `seedPlan` generates every table at once, the client lets you author one
|
|
8
|
+
* table at a time, with autocomplete on each table's columns and live foreign-key
|
|
9
|
+
* connection to whatever was seeded earlier this run. Pass your generated
|
|
10
|
+
* `InsertModel` type as the type argument — or let `lunora/_generated/seed.ts`
|
|
11
|
+
* do that for you — for full type inference on each table's columns.
|
|
12
|
+
*
|
|
13
|
+
* The client is deterministic (same `seed` ⇒ same rows and ids) and pure unless a
|
|
14
|
+
* `persist` hook is supplied, in which case each generated batch is also written
|
|
15
|
+
* through it (the test harness, an admin import, …). Because every row carries an
|
|
16
|
+
* explicit `_id`, the returned ids are known whether or not anything is persisted.
|
|
17
|
+
*/
|
|
18
|
+
/** Picks a concrete count: a fixed number, or a deterministic value within `[min, max]`. */
|
|
19
|
+
type CountHelper = (countOrRange: number | readonly [number, number]) => number;
|
|
20
|
+
/** The per-call population spec: a count, an inclusive range, explicit partial rows, or a count callback. */
|
|
21
|
+
type SeedSpec<Row> = number | ReadonlyArray<Partial<Row>> | readonly [number, number] | ((x: CountHelper) => number);
|
|
22
|
+
/** A per-field override: a fixed value or a function of the row context. */
|
|
23
|
+
type FieldOverride<Value> = ((context: OverrideContext) => Value) | Value;
|
|
24
|
+
/** Options for a single table call. `overrides` win over any explicit partial rows. */
|
|
25
|
+
interface SeedCallOptions<Row> {
|
|
26
|
+
overrides?: { [K in keyof Row]?: FieldOverride<Row[K]> };
|
|
27
|
+
}
|
|
28
|
+
/** The id columns the client emits, keyed by table — the result of a single table call. */
|
|
29
|
+
type SeedCallResult<InsertModel, Table extends keyof InsertModel> = { [K in Table]: string[] };
|
|
30
|
+
/** The seeder function exposed for one table. */
|
|
31
|
+
type TableSeeder<InsertModel, Table extends keyof InsertModel> = (spec?: SeedSpec<InsertModel[Table]>, options?: SeedCallOptions<InsertModel[Table]>) => Promise<SeedCallResult<InsertModel, Table>>;
|
|
32
|
+
/** The `$`-prefixed run state shared by every client. */
|
|
33
|
+
interface SeedClientState {
|
|
34
|
+
/** Every id generated this run, keyed by table (includes auto-seeded FK parents). */
|
|
35
|
+
readonly $ids: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
36
|
+
/** Clear all accumulated rows/ids so the client can drive a fresh run. */
|
|
37
|
+
$reset: () => void;
|
|
38
|
+
/** Every row generated this run, keyed by table. */
|
|
39
|
+
readonly $store: Readonly<Record<string, ReadonlyArray<Record<string, unknown>>>>;
|
|
40
|
+
}
|
|
41
|
+
/** The per-table seeder methods plus the `$`-prefixed run state. */
|
|
42
|
+
type SeedClient<InsertModel> = SeedClientState & { [Table in keyof InsertModel]: TableSeeder<InsertModel, Table> };
|
|
43
|
+
/** Options for {@link createSeedClient}. */
|
|
44
|
+
interface SeedClientOptions {
|
|
45
|
+
/** Default row count when a table call passes no spec (default `10`). */
|
|
46
|
+
defaultCount?: number;
|
|
47
|
+
/** Persist each generated batch (e.g. insert through the test harness). Pure when omitted. */
|
|
48
|
+
persist?: (table: string, rows: ReadonlyArray<Record<string, unknown>>) => Promise<void> | void;
|
|
49
|
+
/** Deterministic mapping selector — same seed ⇒ same rows and ids. Default `0`. */
|
|
50
|
+
seed?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a typed seed client for `schema`. Each table is a method; call it with a
|
|
54
|
+
* count, a range, or explicit partial rows, and its foreign keys connect to rows
|
|
55
|
+
* seeded earlier this run. State accumulates on `$store`/`$ids` and clears with
|
|
56
|
+
* `$reset()`.
|
|
57
|
+
* @example
|
|
58
|
+
* const seed = createSeedClient<InsertModel>(schema, { seed: 1 });
|
|
59
|
+
* const { users } = await seed.users(5);
|
|
60
|
+
* const { posts } = await seed.posts((x) => x([10, 20]));
|
|
61
|
+
* // posts.authorId values are drawn from `users`.
|
|
62
|
+
*/
|
|
63
|
+
declare const createSeedClient: <InsertModel = Record<string, Record<string, unknown>>>(schema: Schema, options?: SeedClientOptions) => SeedClient<InsertModel>;
|
|
64
|
+
export { type CountHelper, type OverrideContext, type SeedCallOptions, type SeedCallResult, type SeedClient, type SeedClientOptions, type SeedSpec, createSeedClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Schema } from '@lunora/server';
|
|
2
|
+
import { O as OverrideContext } from "./packem_shared/plan.d-BQ6LiWIk.js";
|
|
3
|
+
export { type S as SeedCounts, type a as SeedOptions, type b as SeedOverrides, type T as TablePlan, s as seedPlan } from "./packem_shared/plan.d-BQ6LiWIk.js";
|
|
4
|
+
/**
|
|
5
|
+
* A typed, schema-aware seed client — the ergonomic DX layer over {@link seedPlan}.
|
|
6
|
+
*
|
|
7
|
+
* Where `seedPlan` generates every table at once, the client lets you author one
|
|
8
|
+
* table at a time, with autocomplete on each table's columns and live foreign-key
|
|
9
|
+
* connection to whatever was seeded earlier this run. Pass your generated
|
|
10
|
+
* `InsertModel` type as the type argument — or let `lunora/_generated/seed.ts`
|
|
11
|
+
* do that for you — for full type inference on each table's columns.
|
|
12
|
+
*
|
|
13
|
+
* The client is deterministic (same `seed` ⇒ same rows and ids) and pure unless a
|
|
14
|
+
* `persist` hook is supplied, in which case each generated batch is also written
|
|
15
|
+
* through it (the test harness, an admin import, …). Because every row carries an
|
|
16
|
+
* explicit `_id`, the returned ids are known whether or not anything is persisted.
|
|
17
|
+
*/
|
|
18
|
+
/** Picks a concrete count: a fixed number, or a deterministic value within `[min, max]`. */
|
|
19
|
+
type CountHelper = (countOrRange: number | readonly [number, number]) => number;
|
|
20
|
+
/** The per-call population spec: a count, an inclusive range, explicit partial rows, or a count callback. */
|
|
21
|
+
type SeedSpec<Row> = number | ReadonlyArray<Partial<Row>> | readonly [number, number] | ((x: CountHelper) => number);
|
|
22
|
+
/** A per-field override: a fixed value or a function of the row context. */
|
|
23
|
+
type FieldOverride<Value> = ((context: OverrideContext) => Value) | Value;
|
|
24
|
+
/** Options for a single table call. `overrides` win over any explicit partial rows. */
|
|
25
|
+
interface SeedCallOptions<Row> {
|
|
26
|
+
overrides?: { [K in keyof Row]?: FieldOverride<Row[K]> };
|
|
27
|
+
}
|
|
28
|
+
/** The id columns the client emits, keyed by table — the result of a single table call. */
|
|
29
|
+
type SeedCallResult<InsertModel, Table extends keyof InsertModel> = { [K in Table]: string[] };
|
|
30
|
+
/** The seeder function exposed for one table. */
|
|
31
|
+
type TableSeeder<InsertModel, Table extends keyof InsertModel> = (spec?: SeedSpec<InsertModel[Table]>, options?: SeedCallOptions<InsertModel[Table]>) => Promise<SeedCallResult<InsertModel, Table>>;
|
|
32
|
+
/** The `$`-prefixed run state shared by every client. */
|
|
33
|
+
interface SeedClientState {
|
|
34
|
+
/** Every id generated this run, keyed by table (includes auto-seeded FK parents). */
|
|
35
|
+
readonly $ids: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
36
|
+
/** Clear all accumulated rows/ids so the client can drive a fresh run. */
|
|
37
|
+
$reset: () => void;
|
|
38
|
+
/** Every row generated this run, keyed by table. */
|
|
39
|
+
readonly $store: Readonly<Record<string, ReadonlyArray<Record<string, unknown>>>>;
|
|
40
|
+
}
|
|
41
|
+
/** The per-table seeder methods plus the `$`-prefixed run state. */
|
|
42
|
+
type SeedClient<InsertModel> = SeedClientState & { [Table in keyof InsertModel]: TableSeeder<InsertModel, Table> };
|
|
43
|
+
/** Options for {@link createSeedClient}. */
|
|
44
|
+
interface SeedClientOptions {
|
|
45
|
+
/** Default row count when a table call passes no spec (default `10`). */
|
|
46
|
+
defaultCount?: number;
|
|
47
|
+
/** Persist each generated batch (e.g. insert through the test harness). Pure when omitted. */
|
|
48
|
+
persist?: (table: string, rows: ReadonlyArray<Record<string, unknown>>) => Promise<void> | void;
|
|
49
|
+
/** Deterministic mapping selector — same seed ⇒ same rows and ids. Default `0`. */
|
|
50
|
+
seed?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a typed seed client for `schema`. Each table is a method; call it with a
|
|
54
|
+
* count, a range, or explicit partial rows, and its foreign keys connect to rows
|
|
55
|
+
* seeded earlier this run. State accumulates on `$store`/`$ids` and clears with
|
|
56
|
+
* `$reset()`.
|
|
57
|
+
* @example
|
|
58
|
+
* const seed = createSeedClient<InsertModel>(schema, { seed: 1 });
|
|
59
|
+
* const { users } = await seed.users(5);
|
|
60
|
+
* const { posts } = await seed.posts((x) => x([10, 20]));
|
|
61
|
+
* // posts.authorId values are drawn from `users`.
|
|
62
|
+
*/
|
|
63
|
+
declare const createSeedClient: <InsertModel = Record<string, Record<string, unknown>>>(schema: Schema, options?: SeedClientOptions) => SeedClient<InsertModel>;
|
|
64
|
+
export { type CountHelper, type OverrideContext, type SeedCallOptions, type SeedCallResult, type SeedClient, type SeedClientOptions, type SeedSpec, createSeedClient };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { a as setHashKey, s as seedPlan, c as copycat } from './plan-DirmkctQ.mjs';
|
|
2
|
+
|
|
3
|
+
const isRange = (spec) => Array.isArray(spec) && spec.length === 2 && typeof spec[0] === "number" && typeof spec[1] === "number";
|
|
4
|
+
const resolveSpec = (spec, table, seed, defaultCount) => {
|
|
5
|
+
const pick = (countOrRange) => typeof countOrRange === "number" ? countOrRange : copycat.int([seed, table, "count"], { max: countOrRange[1], min: countOrRange[0] });
|
|
6
|
+
if (spec === void 0) {
|
|
7
|
+
return { count: defaultCount };
|
|
8
|
+
}
|
|
9
|
+
if (typeof spec === "number") {
|
|
10
|
+
return { count: spec };
|
|
11
|
+
}
|
|
12
|
+
if (typeof spec === "function") {
|
|
13
|
+
return { count: spec(pick) };
|
|
14
|
+
}
|
|
15
|
+
if (isRange(spec)) {
|
|
16
|
+
return { count: pick(spec) };
|
|
17
|
+
}
|
|
18
|
+
return { count: spec.length, partials: spec };
|
|
19
|
+
};
|
|
20
|
+
const buildOverrides = (partials, fieldOverrides, offset) => {
|
|
21
|
+
const overrides = {};
|
|
22
|
+
if (partials !== void 0) {
|
|
23
|
+
const fields = new Set(partials.flatMap((partial) => Object.keys(partial)));
|
|
24
|
+
for (const field of fields) {
|
|
25
|
+
overrides[field] = (context) => partials[context.index - offset]?.[field];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const [field, value] of Object.entries(fieldOverrides ?? {})) {
|
|
29
|
+
overrides[field] = value;
|
|
30
|
+
}
|
|
31
|
+
return overrides;
|
|
32
|
+
};
|
|
33
|
+
const createSeedClient = (schema, options = {}) => {
|
|
34
|
+
const { defaultCount = 10, persist, seed = 0 } = options;
|
|
35
|
+
const store = {};
|
|
36
|
+
const idsByTable = {};
|
|
37
|
+
const createdCount = {};
|
|
38
|
+
const seedTable = async (table, spec, callOptions) => {
|
|
39
|
+
setHashKey(seed);
|
|
40
|
+
const { count, partials } = resolveSpec(spec, table, seed, defaultCount);
|
|
41
|
+
const offset = createdCount[table] ?? 0;
|
|
42
|
+
const plan = seedPlan(schema, {
|
|
43
|
+
counts: { [table]: count },
|
|
44
|
+
existingIds: idsByTable,
|
|
45
|
+
indexOffset: { [table]: offset },
|
|
46
|
+
only: [table],
|
|
47
|
+
overrides: { [table]: buildOverrides(partials, callOptions?.overrides, offset) },
|
|
48
|
+
seed
|
|
49
|
+
});
|
|
50
|
+
let created = [];
|
|
51
|
+
for (const { rows, table: planned } of plan) {
|
|
52
|
+
const ids = rows.map((row) => row._id);
|
|
53
|
+
store[planned] ??= [];
|
|
54
|
+
store[planned].push(...rows);
|
|
55
|
+
idsByTable[planned] ??= [];
|
|
56
|
+
idsByTable[planned].push(...ids);
|
|
57
|
+
createdCount[planned] = (createdCount[planned] ?? 0) + rows.length;
|
|
58
|
+
if (planned === table) {
|
|
59
|
+
created = ids;
|
|
60
|
+
}
|
|
61
|
+
if (persist !== void 0) {
|
|
62
|
+
await persist(planned, rows);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { [table]: created };
|
|
66
|
+
};
|
|
67
|
+
const state = {
|
|
68
|
+
$ids: idsByTable,
|
|
69
|
+
$reset: () => {
|
|
70
|
+
for (const key of Object.keys(store)) {
|
|
71
|
+
delete store[key];
|
|
72
|
+
}
|
|
73
|
+
for (const key of Object.keys(idsByTable)) {
|
|
74
|
+
delete idsByTable[key];
|
|
75
|
+
}
|
|
76
|
+
for (const key of Object.keys(createdCount)) {
|
|
77
|
+
delete createdCount[key];
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
$store: store
|
|
81
|
+
};
|
|
82
|
+
return /* @__PURE__ */ new Proxy(state, {
|
|
83
|
+
get(target, property, receiver) {
|
|
84
|
+
if (typeof property === "string" && !property.startsWith("$") && Object.hasOwn(schema.tables, property)) {
|
|
85
|
+
return (spec, callOptions) => seedTable(property, spec, callOptions);
|
|
86
|
+
}
|
|
87
|
+
return Reflect.get(target, property, receiver);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export { createSeedClient };
|