@lunora/seed 1.0.0-alpha.27 → 1.0.0-alpha.271
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 +6 -4
- package/dist/index.d.mts +12 -2
- package/dist/index.d.ts +12 -2
- package/dist/index.mjs +1 -2
- package/dist/packem_shared/createSeedClient-DfU2eElm.mjs +1 -0
- package/dist/packem_shared/plan-BO3-4tGm.mjs +1 -0
- package/dist/packem_shared/{plan.d-SaRxbrrI.d.mts → plan.d-CpYSZG6V.d.mts} +16 -0
- package/dist/packem_shared/{plan.d-SaRxbrrI.d.ts → plan.d-CpYSZG6V.d.ts} +16 -0
- package/dist/packem_shared/seedPlan-EMC0cxR1.mjs +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.mjs +1 -53
- package/package.json +4 -3
- package/dist/packem_shared/createSeedClient-CZuGqXLm.mjs +0 -101
- package/dist/packem_shared/plan-DybYaa3F.mjs +0 -526
- package/dist/packem_shared/seedPlan-DbOn48o6.mjs +0 -2
package/README.md
CHANGED
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
|
|
37
37
|
`@lunora/seed` populates a Lunora database with realistic, production-like fake data derived from your `defineSchema`. It introspects every table, maps each field to a generator (field-name aware — a `string` column called `email` becomes an email address, `firstName` a first name, and so on), resolves foreign keys by inserting parent tables before their children, and lets you override any value.
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Generated addresses always sit on the RFC 2606 reserved domain `example.com`, which accepts no mail — a seeded row can never turn into real email to a real stranger when a welcome job, a digest or an auth verification runs over it. Override the column when you need a domain of your own.
|
|
40
|
+
|
|
41
|
+
Generation is **deterministic**: it is built on a vendored, input-hashed generator (a rebuilt [`copycat`](https://github.com/supabase-community/copycat)) layered over [`@faker-js/faker`](https://fakerjs.dev). Generation is deterministic in `seed` alone for every column except the time-valued ones: a `number` column named like a timestamp (`createdAt`, `expiresAt`, …) is generated relative to the wall clock, so two runs with the same `seed` differ unless you also pin `now` (`--now` on the CLI). Ids are unaffected. Pin the `(seed, now)` pair and a plan is byte-identical across runs and machines.
|
|
40
42
|
|
|
41
43
|
Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-safe, real-time backend on Cloudflare Workers + Durable Objects with a Vite-first DX.
|
|
42
44
|
|
|
@@ -121,16 +123,16 @@ Supply a `persist` hook (`SeedClientOptions.persist`) to write each batch as it
|
|
|
121
123
|
```bash
|
|
122
124
|
lunora seed --count 25 # 25 rows per table (default 10)
|
|
123
125
|
lunora seed --table posts --count 100 # one table (FK parents seeded automatically)
|
|
124
|
-
lunora seed --seed 42
|
|
126
|
+
lunora seed --seed 42 --now 1750000000000 # byte-identical run (pin both)
|
|
125
127
|
lunora seed --reset # wipe local .wrangler/state first (local dev only)
|
|
126
128
|
lunora seed --dry-run # print NDJSON, write nothing
|
|
127
129
|
```
|
|
128
130
|
|
|
129
|
-
Also: `--batch-size` (rows per HTTP request, default 500), `--url` (worker URL, default `http://localhost:8787`), `--prod` (requires an explicit `--url`), `--token` / `LUNORA_ADMIN_TOKEN` (admin bearer; prefer the env var), `--yes` (skip the non-local confirmation prompt). `--reset` is local-only and cannot be combined with `--prod` or a remote `--url`.
|
|
131
|
+
Also: `--allow-orphans` (let a required foreign key with no parent row take a dangling id instead of failing), `--batch-size` (rows per HTTP request, default 500), `--url` (worker URL, default `http://localhost:8787`), `--prod` (requires an explicit `--url`), `--token` / `LUNORA_ADMIN_TOKEN` (admin bearer; prefer the env var), `--yes` (skip the non-local confirmation prompt). `--reset` is local-only and cannot be combined with `--prod` or a remote `--url`.
|
|
130
132
|
|
|
131
133
|
## Limitations
|
|
132
134
|
|
|
133
|
-
- **`.unique()` columns are
|
|
135
|
+
- **`.unique()` columns are dealt distinct values by construction.** Each value is a function of the row's absolute index, not a hash of the row, so two rows in the same run never collide — including across the several calls `indexOffset` exists to support. A column whose domain is too small to cover the requested count (a boolean, a three-literal `v.union()`) is refused at plan time, with the column named, before anything is inserted.
|
|
134
136
|
- **Seeding is deterministic by design.** Re-running with the same `--seed` regenerates identical `_id`s, which the import path skips as conflicts. Use a different `--seed` for fresh rows, or `--reset` to wipe local state first.
|
|
135
137
|
|
|
136
138
|
> This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/packages/seed)**.
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from '@lunora/server';
|
|
2
|
-
import { O as OverrideContext } from "./packem_shared/plan.d-
|
|
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-
|
|
2
|
+
import { O as OverrideContext } from "./packem_shared/plan.d-CpYSZG6V.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-CpYSZG6V.mjs";
|
|
4
4
|
/**
|
|
5
5
|
* A typed, schema-aware seed client — the ergonomic DX layer over {@link seedPlan}.
|
|
6
6
|
*
|
|
@@ -42,8 +42,18 @@ interface SeedClientState {
|
|
|
42
42
|
type SeedClient<InsertModel> = SeedClientState & { [Table in keyof InsertModel]: TableSeeder<InsertModel, Table>; };
|
|
43
43
|
/** Options for {@link createSeedClient}. */
|
|
44
44
|
interface SeedClientOptions {
|
|
45
|
+
/** Let a required foreign key with no parent row take a dangling id instead of refusing (see `SeedOptions.allowOrphans`). */
|
|
46
|
+
allowOrphans?: boolean;
|
|
45
47
|
/** Default row count when a table call passes no spec (default `10`). */
|
|
46
48
|
defaultCount?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Wall-clock reference for time-valued columns (`createdAt`, `expiresAt`, a
|
|
51
|
+
* `.unique()` date, …), in epoch-ms. Defaults to `Date.now()` **per call**,
|
|
52
|
+
* which is the one input that otherwise drifts between runs — pin it and the
|
|
53
|
+
* client is deterministic in the full sense the docs promise, not merely in
|
|
54
|
+
* its ids.
|
|
55
|
+
*/
|
|
56
|
+
now?: number;
|
|
47
57
|
/** Persist each generated batch (e.g. insert through the test harness). Pure when omitted. */
|
|
48
58
|
persist?: (table: string, rows: ReadonlyArray<Record<string, unknown>>) => Promise<void> | void;
|
|
49
59
|
/** Deterministic mapping selector — same seed ⇒ same rows and ids. Default `0`. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema } from '@lunora/server';
|
|
2
|
-
import { O as OverrideContext } from "./packem_shared/plan.d-
|
|
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-
|
|
2
|
+
import { O as OverrideContext } from "./packem_shared/plan.d-CpYSZG6V.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-CpYSZG6V.js";
|
|
4
4
|
/**
|
|
5
5
|
* A typed, schema-aware seed client — the ergonomic DX layer over {@link seedPlan}.
|
|
6
6
|
*
|
|
@@ -42,8 +42,18 @@ interface SeedClientState {
|
|
|
42
42
|
type SeedClient<InsertModel> = SeedClientState & { [Table in keyof InsertModel]: TableSeeder<InsertModel, Table>; };
|
|
43
43
|
/** Options for {@link createSeedClient}. */
|
|
44
44
|
interface SeedClientOptions {
|
|
45
|
+
/** Let a required foreign key with no parent row take a dangling id instead of refusing (see `SeedOptions.allowOrphans`). */
|
|
46
|
+
allowOrphans?: boolean;
|
|
45
47
|
/** Default row count when a table call passes no spec (default `10`). */
|
|
46
48
|
defaultCount?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Wall-clock reference for time-valued columns (`createdAt`, `expiresAt`, a
|
|
51
|
+
* `.unique()` date, …), in epoch-ms. Defaults to `Date.now()` **per call**,
|
|
52
|
+
* which is the one input that otherwise drifts between runs — pin it and the
|
|
53
|
+
* client is deterministic in the full sense the docs promise, not merely in
|
|
54
|
+
* its ids.
|
|
55
|
+
*/
|
|
56
|
+
now?: number;
|
|
47
57
|
/** Persist each generated batch (e.g. insert through the test harness). Pure when omitted. */
|
|
48
58
|
persist?: (table: string, rows: ReadonlyArray<Record<string, unknown>>) => Promise<void> | void;
|
|
49
59
|
/** Deterministic mapping selector — same seed ⇒ same rows and ids. Default `0`. */
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { s as seedPlan } from './packem_shared/plan-DybYaa3F.mjs';
|
|
1
|
+
import{createSeedClient as o}from"./packem_shared/createSeedClient-DfU2eElm.mjs";import{s as a}from"./packem_shared/plan-BO3-4tGm.mjs";export{o as createSeedClient,a as seedPlan};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as C,s as P,c as T}from"./plan-BO3-4tGm.mjs";const A=e=>Array.isArray(e)&&e.length===2&&typeof e[0]=="number"&&typeof e[1]=="number",q=(e,d,u,i)=>{const s=n=>typeof n=="number"?n:T.int([u,d,"count"],{max:n[1],min:n[0]});return e===void 0?{count:i}:typeof e=="number"?{count:e}:typeof e=="function"?{count:e(s)}:A(e)?{count:s(e)}:{count:e.length,partials:e}},B=(e,d,u)=>{const i={};if(e!==void 0){const s=new Set(e.flatMap(n=>Object.keys(n)));for(const n of s)i[n]=l=>e[l.index-u]?.[n]}for(const[s,n]of Object.entries(d??{}))i[s]=n;return i},I=(e,d={})=>{const{allowOrphans:u=!1,defaultCount:i=10,now:s,persist:n,seed:l=0}=d,a={},f={},y={},j=async(t,o,v)=>{C(l);const{count:c,partials:b}=q(o,t,l,i),k=y[t]??0,S=P(e,{allowOrphans:u,counts:{[t]:c},existingIds:f,indexOffset:{[t]:k},now:s,only:[t],overrides:{[t]:B(b,v?.overrides,k)},seed:l});let w=[];for(const{rows:h,table:r}of S){const O=h.map($=>$._id);a[r]??=[],a[r].push(...h),f[r]??=[],f[r].push(...O),y[r]=(y[r]??0)+h.length,r===t&&(w=O),n!==void 0&&await n(r,h)}return{[t]:w}};let m=Promise.resolve();const x=(t,o,v)=>{const c=m.then(()=>j(t,o,v));return m=c.then(()=>{},()=>{}),c},g={$ids:f,$reset:()=>{for(const t of Object.keys(a))delete a[t];for(const t of Object.keys(f))delete f[t];for(const t of Object.keys(y))delete y[t]},$store:a};return new Proxy(g,{get(t,o,v){return typeof o=="string"&&!o.startsWith("$")&&Object.hasOwn(e.tables,o)?(c,b)=>x(o,c,b):Reflect.get(t,o,v)}})};export{I as createSeedClient};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as T}from"@lunora/errors";import{faker as g}from"@faker-js/faker";import{cyrb53 as we,optionalInner as be}from"@lunora/values";let K=0;const Oe=(e,t)=>e<t?-1:e>t?1:0,ee=e=>e===void 0?"undefined":typeof e=="bigint"?`${e.toString()}n`:e===null||typeof e!="object"?JSON.stringify(e)??"null":Array.isArray(e)?`[${e.map(n=>ee(n)).join(",")}]`:`{${Object.keys(e).toSorted(Oe).map(n=>`${JSON.stringify(n)}:${ee(e[n])}`).join(",")}}`,De="\0",Re=e=>{const t=typeof e=="string"?De+e:ee(e);return we(t,K)%4294967296},Le=e=>{if(typeof e=="number"){K=e>>>0;return}if(typeof e=="string"){K=we(e,0)%4294967296;return}let t=0;for(const n of e)t=Math.imul(t^n,2654435761)>>>0;K=t},v=(e,t)=>(g.seed(Re(e)),t()),Fe=(e,t)=>{if(typeof t=="number")return t;const[n,r]=t;return v(["__count__",e],()=>g.number.int({max:r,min:n}))},Pe="example.com",qe=/[a-z]/,Ce=/[A-Z]/,Ue=/\d/,m={bool(e){return v(e,()=>g.datatype.boolean())},city(e){return v(e,()=>g.location.city())},country(e){return v(e,()=>g.location.country())},email(e){return v(e,()=>g.internet.email({provider:Pe}).toLowerCase())},firstName(e){return v(e,()=>g.person.firstName())},float(e,t){const{fractionDigits:n=2,max:r=1e3,min:a=0}=t??{};return v(e,()=>g.number.float({fractionDigits:n,max:r,min:a}))},fullName(e){return v(e,()=>g.person.fullName())},int(e,t){const{max:n=1e3,min:r=0}=t??{};return v(e,()=>g.number.int({max:n,min:r}))},lastName(e){return v(e,()=>g.person.lastName())},oneOf(e,t){if(t.length!==0)return v(e,()=>g.helpers.arrayElement(t))},paragraph(e){return v(e,()=>g.lorem.paragraph())},password(e){return v(e,()=>g.internet.password())},phoneNumber(e){return v(e,()=>g.phone.number())},scramble(e,t){const n=new Set(t?.preserve);return Array.from({length:e.length},(r,a)=>{const o=e.charAt(a);return n.has(o)?o:qe.test(o)?v([e,a,"l"],()=>g.string.alpha({casing:"lower",length:1})):Ce.test(o)?v([e,a,"u"],()=>g.string.alpha({casing:"upper",length:1})):Ue.test(o)?v([e,a,"d"],()=>String(g.number.int({max:9,min:0}))):o}).join("")},sentence(e,t){return v(e,()=>g.lorem.sentence(t?{max:t.max??8,min:t.min??3}:void 0))},slug(e){return v(e,()=>g.lorem.slug())},streetAddress(e){return v(e,()=>g.location.streetAddress())},times(e,t,n){const r=Fe(e,t),a=[];for(let o=0;o<r;o+=1)a.push(n([e,o]));return a},url(e){return v(e,()=>g.internet.url())},username(e){return v(e,()=>g.internet.username())},uuid(e){return v(e,()=>g.string.uuid())},word(e){return v(e,()=>g.lorem.word())}},I=e=>e._meta??{},ne=e=>be(e)??e,Ie=e=>{const{column:t}=I(e);if(t?.defaultValue!==void 0||t?.defaultFn!==void 0)return!0;const n=be(e);return n===void 0?!1:Ie(n)},Ve=(e,t)=>{const n=ne(t),r=I(n);return{fkTable:n.kind==="id"?r.tableName:void 0,hasServerDefault:Ie(t),kind:n.kind,name:e,nullable:r.column?.notNull===!1,optional:t.kind==="optional",unique:r.column?.unique===!0,uniqueFromIndex:!1,validator:n}},je=e=>{const{tables:t}=e;return Object.entries(t).map(([n,r])=>{const a=r.indexes.filter(s=>s.unique),o=new Set(a.filter(s=>s.fields.length===1).map(s=>s.fields[0]));return{fields:Object.entries(r.shape).map(([s,u])=>{const i=Ve(s,u);return o.has(s)&&!i.unique?{...i,unique:!0,uniqueFromIndex:!0}:i}),name:n,uniqueIndexes:a.filter(s=>s.fields.length>1).map(s=>({fields:s.fields,name:s.name}))}})},Be=(e,t)=>{const n=new Map(e.map(i=>[i.name,i])),r=i=>{const c=n.get(i);if(c===void 0)return new Set;const f=new Set;for(const d of c.fields)d.fkTable!==void 0&&d.fkTable!==i&&t.has(d.fkTable)&&f.add(d.fkTable);return f},a=[],o=new Set,s=[...t].filter(i=>n.has(i)),u=i=>{const c=new Set,f=[...r(i)];for(;f.length>0;){const d=f.pop();if(d===i)return!0;!o.has(d)&&!c.has(d)&&(c.add(d),f.push(...r(d)))}return!1};for(;s.length>0;){const i=s.findIndex(d=>[...r(d)].every(p=>o.has(p))),c=i===-1?s.findIndex(d=>u(d)):i,[f]=s.splice(c,1);if(f===void 0)break;a.push(f),o.add(f)}return a},Ye=(e,t,n=new Set)=>{const r=new Map(e.map(s=>[s.name,s])),a=new Set(t),o=[...a];for(;o.length>0;){const s=o.pop();if(s===void 0)break;const u=r.get(s);if(u!==void 0&&!n.has(s))for(const i of u.fields)i.fkTable!==void 0&&i.fkTable!==s&&r.has(i.fkTable)&&!a.has(i.fkTable)&&(a.add(i.fkTable),o.push(i.fkTable))}return a},Ge=1e-6,He=(e,t,n)=>{const r=n?Math.floor(e)+1:e+Ge,a=r>e?r:e+Math.abs(e)*Number.EPSILON;if(n||t===void 0||a<=t||t<=e)return a;const o=(e+t)/2;return o>e?o:t},G=e=>{const t=I(e).constraints??{},n=t.type==="integer";let{maximum:r,minimum:a}=t;if(typeof t.exclusiveMinimum=="number"){const o=He(t.exclusiveMinimum,r,n);a=a===void 0?o:Math.max(a,o)}return n&&(a=a===void 0?void 0:Math.ceil(a),r=r===void 0?void 0:Math.floor(r)),{...t,maximum:r,minimum:a,minimumFromExclusive:t.minimum===void 0&&typeof t.exclusiveMinimum=="number"}},Qe=[{generate:e=>m.email(e),keywords:["email"]},{generate:e=>m.firstName(e),keywords:["firstname"]},{generate:e=>m.lastName(e),keywords:["lastname","surname"]},{generate:e=>m.username(e),keywords:["username"]},{generate:e=>m.fullName(e),keywords:["name"]},{generate:e=>m.sentence(e,{max:5,min:2}),keywords:["title"]},{generate:e=>m.url(e),keywords:["url","link","image","avatar"]},{generate:e=>m.phoneNumber(e),keywords:["phone"]},{generate:e=>m.paragraph(e),keywords:["description","bio","body","content","text"]},{generate:e=>m.slug(e),keywords:["slug","key","code"]},{generate:e=>m.city(e),keywords:["city"]},{generate:e=>m.country(e),keywords:["country"]},{generate:e=>m.streetAddress(e),keywords:["address","street"]},{generate:e=>m.password(e),keywords:["password","secret","token"]}],We=new Set(["at","date","deadline","expires","expiry","since","timestamp","until"]),Ke=/([a-z\d])([A-Z])/gu,Xe=/[\s_-]+/u,ze=e=>e.replaceAll(Ke,"$1 $2").split(Xe).filter(t=>t!=="").map(t=>t.toLowerCase()),Se={max:1e3,min:0},Ae={max:1e6,min:0},Je=4320*60*60*1e3,re=(e,t,n)=>{const{maximum:r,minimum:a}=e;if(r!==void 0&&a!==void 0&&a>r)throw new T("INTERNAL",`Seed constraint error for field "${n}": minimum (${String(a)}) > maximum (${String(r)}). Adjust the schema constraints.`);const o=t.max-t.min;return{max:r??(a!==void 0&&a>t.max?a+o:t.max),min:a??(r!==void 0&&r<t.min?r-o:t.min)}},Ze=e=>{const n=ze(e).at(-1);return n!==void 0&&We.has(n)},Te=(e,t)=>e.maximum===void 0&&(e.minimum===void 0||e.minimumFromExclusive===!0&&(e.exclusiveMinimum??0)<=0)&&Ze(t),ue=(e,t)=>t-m.int(e,{max:Je,min:0}),V=(e,t)=>{throw new T("INTERNAL",`@lunora/seed: column "${e}" ${t}. Supply a value via \`overrides\` (\`{ <table>: { ${e}: … } }\`) or restrict the run with \`only\` so the table is skipped.`)},ae=(e,t)=>{const n=e===""?"x":e,r=Math.min(Math.max(n.length,t.min),t.max);return n.padEnd(r,n).slice(0,r)},j="@example.com",X="https://",et=/[\s@]/gu,tt=/[^a-z\d]/gu,nt=(e,t)=>{const n=e.lastIndexOf("@"),r=n>0?e.slice(n):"",a=r.length>0&&r.length<t.max?r:j,o=(n>0?e.slice(0,n):e).replaceAll(et,"");return`${ae(o,{max:t.max-a.length,min:Math.max(1,t.min-a.length)})}${a}`},rt=(e,t)=>{const n=URL.canParse(e)?new URL(e).hostname.replaceAll(tt,""):"";return`${X}${ae(n,{max:t.max-X.length,min:Math.max(1,t.min-X.length)})}`},at={email:{fit:nt,generate:e=>m.email(e),minimum:j.length+1},uri:{fit:rt,generate:e=>m.url(e),minimum:X.length+1}},ot=(e,t)=>{const n=e.toLowerCase(),r=Qe.find(a=>a.keywords.some(o=>n.includes(o)));return r===void 0?m.word(t):r.generate(t)},st=(e,t,n)=>{const{format:r,maxLength:a,minLength:o,pattern:s}=n;if(s!==void 0)return V(e,`is constrained to the pattern /${s}/, and the seeder cannot invent a value matching an arbitrary regular expression`);if(a!==void 0&&o!==void 0&&o>a)throw new T("INTERNAL",`Seed constraint error for field "${e}": minLength (${String(o)}) > maxLength (${String(a)}). Adjust the schema constraints.`);const u={max:a??Number.POSITIVE_INFINITY,min:o??0};if(r===void 0)return ae(ot(e,t),u);const i=at[r];if(i===void 0)return V(e,`declares format "${r}", which the seeder has no generator for`);if(u.max<i.minimum)return V(e,`declares format "${r}" with maxLength ${String(a)}, and the shortest ${r} value the seeder can build is ${String(i.minimum)} characters`);const c=i.generate(t);return c.length>=u.min&&c.length<=u.max?c:i.fit(c,u)},it=(e,t,n)=>{const{max:r,min:a}=re(t,Se,n);return t.type==="integer"||Number.isInteger(a)&&Number.isInteger(r)?m.int(e,{max:r,min:a}):Math.ceil(a*100)>Math.floor(r*100)?a+(r-a)*m.float(e,{fractionDigits:6,max:1,min:0}):m.float(e,{max:r,min:a})},Ne=(e,t)=>{const n=e.minItems===void 0?void 0:Math.max(0,Math.ceil(e.minItems)),r=e.maxItems===void 0||e.maxItems===Number.POSITIVE_INFINITY?void 0:Math.floor(e.maxItems),a=n??Math.max(0,Math.min(1,r??1)),o=r??Math.max(3,a);return o<a?V(t,`declares minItems ${String(e.minItems)} and maxItems ${String(e.maxItems)}, which no whole array length satisfies`):[a,o]},D=(e,t,n,r)=>{const a=ne(e),o=G(a);if(o.enum!==void 0&&o.enum.length>0)return m.oneOf(n,o.enum);switch(a.kind){case"any":return m.word(n);case"array":{const s=I(a).inner;if(s===void 0)return[];const[u,i]=Ne(o,t);return m.times(n,[u,i],c=>D(s,t,c,r))}case"bigint":return m.int(n,re(o,Ae,t));case"boolean":return m.bool(n);case"bytes":return Array.from({length:8},(s,u)=>m.int([n,u],{max:255,min:0}));case"date":case"timestamp":return ue(n,r);case"from":return V(t,"is a v.from() validator, whose external Standard Schema the seeder cannot introspect to invent a conforming value (give the column a concrete v.* type instead)");case"id":return m.uuid(n);case"literal":return I(a).value;case"null":return null;case"number":return Te(o,t)?ue(n,r):it(n,o,t);case"object":{const s=I(a).shape??{};return Object.fromEntries(Object.entries(s).map(([u,i])=>[u,D(i,u,[n,u],r)]))}case"record":{const{keyValidator:s,valueValidator:u}=I(a),i=m.times(n,[1,3],c=>{const f=s===void 0?m.word(["k",c]):String(D(s,t,["k",c],r)),d=u===void 0?m.word(["v",c]):D(u,t,["v",c],r);return[f,d]});return Object.fromEntries(i)}case"storage":return`seed/${m.uuid(n)}`;case"string":return st(t,n,o);case"union":{const s=I(a).members??[],u=m.oneOf(n,s);return u===void 0?m.word(n):D(u,t,[n,"u"],r)}default:return m.word(n)}},Q=1e6,ct=4,z=new DataView(new ArrayBuffer(8)),te=2n**63n,le=e=>{z.setFloat64(0,e);const t=z.getBigUint64(0);return t>=te?-(t-te):t},ut=e=>(z.setBigUint64(0,e<0n?te-e:e),z.getFloat64(0)),lt=6e4,mt=2**32,dt=(e,t,n)=>{const r=[...e];for(let a=r.length-1;a>0;a-=1){const o=m.int([t,n,"unique-deal",a],{max:a,min:0}),s=r[a];r[a]=r[o],r[o]=s}return r},ht=(e,t,n)=>{const{maxLength:r}=n,a=String(t),o=e.indexOf("@");if(o>0){const i=e.slice(0,o),c=e.slice(o),f=r===void 0?i.length:r-c.length-a.length-1;if(f>=0)return`${i.slice(0,f)}+${a}${c}`}if(n.format==="email"){const i=r===void 0?e.length:Math.max(0,r-j.length-a.length);return`${e.slice(0,i)}${a}${j}`}const s=`-${a}`,u=r===void 0?e.length:Math.max(0,r-s.length);return`${e.slice(0,u)}${s}`},U=(e,t,n)=>{const r=dt([...new Set(e)],t,n);return{capacity:r.length,valueAt:a=>r[a%r.length]}},ft=(e,t)=>{if(Number.isInteger(e)&&Number.isInteger(t)){let o=1;for(;Math.max(Math.abs(e),Math.abs(t))/o>=2**53;)o*=2;const s=Math.ceil(e/o)*o,u=Math.floor(t/o)*o;if(s>u)return{capacity:1,valueAt:()=>e};const i=(u-s)/o+1;return{capacity:i,valueAt:c=>s+c%i*o}}if(e===t)return{capacity:1,valueAt:()=>e};const n=Math.max(Math.max(Math.abs(e),Math.abs(t))*Number.EPSILON,Number.MIN_VALUE);if((t-e)/Q>=ct*n)return{capacity:Q,valueAt:o=>e+o%Q*(t-e)/Q};const r=le(e),a=Number(le(t)-r)+1;return{capacity:a,valueAt:o=>ut(r+BigInt(o%a))}},me=e=>({capacity:Number.POSITIVE_INFINITY,valueAt:t=>e-t*lt}),gt={capacity:Number.POSITIVE_INFINITY,valueAt:e=>e},B={capacity:Number.POSITIVE_INFINITY,valueAt:(e,t)=>t()},de=8,vt=(e,t)=>{const n=new Set;return{capacity:Number.POSITIVE_INFINITY,valueAt:(r,a)=>{for(let o=0;o<=de;o+=1){const s=a(o);if(!n.has(s))return n.add(s),s}throw new T("BAD_REQUEST",`@lunora/seed: cannot generate unique values for "${e}"."${t}" — the generator's draw and its ${String(de)} redraws all collided with a value an earlier row already took. Supply "${t}" via \`overrides\` (\`{ ${e}: { ${t}: … } }\`).`)}}},he=(e,t,n)=>{if(e.maximum===void 0&&e.minimum===void 0)return gt;if(e.maximum===void 0&&e.minimumFromExclusive===!0){const o=Math.max(Math.floor(e.exclusiveMinimum??0)+1,-Number.MAX_SAFE_INTEGER);return o>Number.MAX_SAFE_INTEGER?{capacity:1,valueAt:()=>e.minimum}:{capacity:Number.POSITIVE_INFINITY,valueAt:s=>o+s}}const{max:r,min:a}=re(e,t,n);return ft(a,r)},_e=(e,t,n)=>{throw new T("INTERNAL",`@lunora/seed: cannot generate unique values for "${e}"."${t}" — ${n}. Supply them via \`overrides\` (\`{ ${e}: { ${t}: … } }\`) or drop \`.unique()\` from the column.`)},pt=({format:e,maxLength:t})=>{if(t===void 0)return Number.POSITIVE_INFINITY;const n=e==="email"?j.length:1;return t<=n?0:10**(t-n)},J=({format:e,pattern:t})=>t!==void 0?"the column is pattern-constrained, and an index-tagged value would no longer match it":e===void 0||e==="email"?void 0:`the column declares format "${e}", which an index-tagged value would no longer satisfy`,yt=(e,t,n)=>{const r=J(e);return r!==void 0&&_e(t,n,r),{capacity:pt(e),valueAt:(a,o)=>ht(String(o()),a,e)}},Ee=(e,t,n)=>U(e,t,n),Y=(e,t)=>t.filter(n=>e.safeParse(n).ok),W=(e,t)=>t===void 0||I(e).column?.notNull!==!1?t:[...new Set([...t,null])],ke=e=>{const t=ne(e),{enum:n}=G(t);if(Array.isArray(n)&&n.length>0)return W(t,[...new Set(n)]);switch(t.kind){case"boolean":return W(t,[!1,!0]);case"literal":return W(t,[I(t).value]);case"null":return[null];case"union":{const r=(I(t).members??[]).map(a=>ke(a));return W(t,r.length>0&&r.every(a=>a!==void 0)?[...new Set(r.flat())]:void 0)}default:return}},oe=e=>{const t=ke(e);return t===void 0?void 0:Y(e,t)},wt=65536,bt=(e,t,n)=>{if(e===0)return t===0?1:0;if(e===1)return Math.max(0,n-t+1);let r=0;for(let a=t;a<=n&&r<=Number.MAX_SAFE_INTEGER;a+=1)r+=e**a;return r>Number.MAX_SAFE_INTEGER?Number.POSITIVE_INFINITY:r},fe=(e,t,n)=>{const r=e.length;if(r===0)return[];if(r===1)return Array.from({length:t+n}).fill(e[0]);let a=n,o=t;for(;a>=r**o;)a-=r**o,o+=1;return Array.from({length:o},(s,u)=>e[Math.floor(a/r**u)%r])},ge=e=>({capacity:e.length,valueAt:t=>e.length===0?void 0:e[t%e.length]}),It=(e,t,n)=>{const r=I(e.validator).inner,[a,o]=Ne(t,e.name);if(r===void 0||o<=0)return ge(Y(e.validator,[[]]));const s=oe(r);if(s===void 0)return B;const u=t.minItems===void 0?0:a,i=t.maxItems===void 0||t.maxItems===Number.POSITIVE_INFINITY?Number.POSITIVE_INFINITY:o,c=bt(s.length,u,i);return c<=wt?ge(Y(e.validator,Array.from({length:c},(f,d)=>fe(s,u,d)))):{capacity:c,valueAt:f=>{const d=fe(s,u,f%c);return e.validator.safeParse(d).ok||_e(n,e.name,`the column's validator rejects the array dealt at row ${String(f)}, and too many arrays exist to check them all up front`),d}}},St=(e,t,n)=>{const r=It(e,t,n);return!e.nullable||!Number.isFinite(r.capacity)?r:{capacity:r.capacity+1,valueAt:(a,o)=>{const s=a%(r.capacity+1);return s===r.capacity?null:r.valueAt(s,o)}}},se=(e,t)=>{const{now:n,table:r}=t,a=G(e.validator),o=a.enum;if(Array.isArray(o)&&o.length>0)return U(Y(e.validator,o),r,e.name);switch(e.kind){case"any":case"string":return e.uniqueFromIndex&&a.pattern===void 0&&J(a)!==void 0?vt(r,e.name):yt(a,r,e.name);case"array":return St(e,a,r);case"bigint":return he(a,Ae,e.name);case"boolean":return U(Y(e.validator,[!1,!0]),r,e.name);case"bytes":return{capacity:mt,valueAt:s=>[Math.floor(s/16777216)%256,Math.floor(s/65536)%256,Math.floor(s/256)%256,s%256,...Array.from({length:4},(u,i)=>m.int([r,e.name,"unique-bytes",i],{max:255,min:0}))]};case"date":case"timestamp":return me(n);case"literal":{const{value:s}=I(e.validator);return U(e.nullable?[s,null]:[s],r,e.name)}case"number":return Te(a,e.name)?me(n):he(a,Se,e.name);case"union":{const s=oe(e.validator);return s===void 0?B:U(s,r,e.name)}default:return B}},At=(e,t)=>{const n=G(e.validator),r=(e.kind==="string"||e.kind==="any")&&!(Array.isArray(n.enum)&&n.enum.length>0);if(r&&J(n)!==void 0)return;const a=se(e,t);if(!(a===B||a.capacity<=1))return{deal:a,rewrites:r||!Number.isFinite(a.capacity)}},Tt=(e,t)=>{const n=G(e.validator);return e.kind==="string"||e.kind==="any"?!(Array.isArray(n.enum)&&n.enum.length>0)&&n.pattern===void 0&&J(n)!==void 0:se(e,t)===B},Me=e=>oe(e.validator)?.includes(null)===!0,ve=(e,t)=>typeof e=="function"?e(t):e,Nt=(e,t,n,r)=>{if(!e.optional){if(e.nullable)return null;if(r)return m.uuid(n);throw new T("BAD_REQUEST",`cannot seed "${t}": required foreign key "${e.name}" points at "${e.fkTable}", which has no rows to reference. Seed "${e.fkTable}" too, make "${e.name}" optional or nullable, supply "${e.name}" yourself, or pass \`allowOrphans: true\` (\`--allow-orphans\` on the CLI) to accept dangling ids.`)}},_t=e=>({get:t=>e[t],length:e.length}),Et=e=>Array.from({length:e.length},(t,n)=>e.get(n)),pe=(e,t)=>t.get(m.int(e,{max:t.length-1,min:0})),kt=(e,t,n,r)=>{const a=e.fkTable,o=r[a]??[];if(a!==t){const u=_t([...new Set([...n.get(a)??[],...o])]);return()=>u}const s=n.get(t)??[];return u=>({get:i=>i<u?s[i]:o[i-u],length:u+o.length})},Mt=(e,t,n,r)=>{const a=r[e]??[],o=n.get(e)??[];if(t<a.length)return a[t];const s=t-a.length;return s<t?o[s]:o[t-1]??a.at(-1)},ye=(e,t,n,r)=>[e,t,n,r],xt=(e,t)=>{const{existingIds:n,idsByTable:r,index:a,input:o,localIndex:s,now:u,pools:i,table:c,uniqueDeals:f}=t;if(e.fkTable!==void 0){const p=i.get(e.name)(s);if(p.length===0)return Nt(e,c,o,t.allowOrphans);const w=f.get(e.name);return!e.unique&&w===void 0?pe(o,p):w===void 0?Mt(c,s,r,n):w.valueAt(a,()=>pe(o,p))}if(e.hasServerDefault)return;const d=f.get(e.name);return d!==void 0?d.valueAt(a,(p=0)=>D(e.validator,e.name,p===0?o:[...o,"unique-redraw",p],u)):D(e.validator,e.name,o,u)},$t=(e,t,n,r)=>{if(!(n(e).length===0||Object.hasOwn(r,e.name)))throw new T("BAD_REQUEST",`cannot seed into the non-empty table "${t}": unique self-referencing column "${e.name}" cannot be dealt across calls, because the values already stored in it are not knowable from the row ids. Seed the table in one call, or supply "${e.name}" yourself.`)},Ot=(e,t,n)=>{const r=[];let a=1;for(const{deal:i}of e)r.push(a),a*=i.capacity;const o=(i,c)=>Math.floor(i/r[c])%e[c].deal.capacity,s=e.map(({deal:i,field:c})=>({capacity:i.capacity,name:c.name})),u=e.findIndex(({field:i})=>Me(i));for(const[i,{deal:c,field:f}]of e.entries())n.set(f.name,{capacity:t,chain:s,valueAt:(d,p)=>{if(i===u&&d>=t)return null;let w=0;for(let N=0;N<i;N+=1)w+=o(d,N);return c.valueAt((o(d,i)+w)%c.capacity,p)}})},Dt=(e,t,n,r,a,o,s,u)=>{const i=[...new Set(e.fields)],c=i.flatMap(l=>t.get(l)??[]),f=l=>l.unique||l.fkTable===n||l.hasServerDefault&&l.fkTable===void 0||l.fkTable!==void 0&&s(l).length===0;if(c.length!==i.length||c.some(l=>f(l)))return;const d=c.filter(l=>u.has(l.name)),p=Math.max(1,...d.map(({name:l})=>{const b=u.get(l)?.chain??[],F=b.findIndex(P=>!i.includes(P.name));return b.slice(0,F===-1?void 0:F).reduce((P,h)=>P*h.capacity,1)})),w=c.filter(l=>!u.has(l.name)).map(l=>{if(l.fkTable!==void 0)return{carrier:{deal:Ee(s(l),n,l.name),field:l},rank:0};const b=At(l,{now:o,table:n});return b===void 0?void 0:{carrier:{deal:b.deal,field:l},rank:b.rewrites?2:1}}).filter(l=>l!==void 0).toSorted((l,b)=>l.rank-b.rank),N=a+r,L=[];let $=1;for(const{carrier:l,rank:b}of w){if(b===2&&Math.max($,p)>=N)break;L.push(l),$*=l.deal.capacity}const _=L.some(({field:l})=>Me(l))?Number.POSITIVE_INFINITY:Math.max($,p);if(N>_&&!c.some(l=>l.fkTable===void 0&&Tt(l,{now:o,table:n})))throw new T("BAD_REQUEST",`cannot seed ${String(N)} rows into "${n}": unique index "${e.name}" over (${e.fields.join(", ")}) has only ${String(_)} distinct combinations`);Ot(L,$,u)},Rt=(e,t,n,r,a,o,s,u)=>{const i=new Map(t.map(c=>[c.name,c]));for(const c of e)Dt(c,i,n,r,a,o,s,u)},Lt=(e,t,n,r)=>{if(t!==void 0)return Ee(t,n.table,e.name);try{return se(e,n)}catch(a){if(r&&a instanceof T)return;throw a}},Ft=(e,t,n,r,a,o,s,u)=>{const i=new Map;for(const c of e){if(!c.unique||c.hasServerDefault&&c.fkTable===void 0)continue;if(c.fkTable===n){$t(c,n,u,s);continue}const f=c.fkTable===void 0?void 0:u(c);if(f?.length===0)continue;const d=Object.hasOwn(s,c.name),p=Lt(c,f,{now:o,table:n},d);if(p===void 0)continue;const w=a+r;if(w>p.capacity&&!d)throw new T("BAD_REQUEST",`cannot seed ${String(w)} rows into "${n}": unique column "${c.name}" has only ${String(p.capacity)} possible values`);i.set(c.name,p)}return Rt(t,e,n,r,a,o,u,i),i},Pt=(e,t)=>{if(t===void 0)return;const n=new Set(e.map(r=>r.name));for(const r of t)if(!n.has(r)){const a=e.map(o=>o.name).join(", ");throw new T("BAD_REQUEST",`unknown table "${r}" in seed \`only\` — schema defines: ${a||"(no tables)"}`)}},Vt=(e,t={})=>{const{allowOrphans:n=!1,counts:r={},defaultCount:a=10,existingIds:o={},indexOffset:s={},now:u=Date.now(),only:i,overrides:c={},seed:f=0}=t;Le(f);const d=je(e);Pt(d,i);const p=new Set(i??d.map(h=>h.name)),w=new Set(Object.keys(o).filter(h=>!p.has(h)&&(o[h]??[]).length>0)),N=new Set([...Ye(d,p,w)].filter(h=>p.has(h)||(o[h]??[]).length===0)),L=Be(d,N),$=new Map(d.map(h=>[h.name,h])),_=new Map,l={},b=[],F=(h,A,y,R=l)=>{const k=c[h]??{};if(Object.hasOwn(k,"_id")){const M=ve(k._id,{field:"_id",index:A,row:y,store:R,table:h});if(M!==void 0)return M}return m.uuid(ye(f,h,A,"_id"))},P=h=>{for(const A of h.fields){const y=A.fkTable;if(y!==void 0&&y!==h.name&&N.has(y)&&$.has(y)&&!_.has(y)){const R=s[y]??0,k=[],M={...l,[y]:k},q=[];for(let O=0;O<(r[y]??a);O+=1){const C=F(y,R+O,{},M);q.push(C),k.push({_id:C})}_.set(y,q)}}};for(const h of L){const A=$.get(h);if(A===void 0)continue;const y=c[h]??{},R=r[h]??a,k=s[h]??0;P(A);const M=_.get(h),q=M??[];_.set(h,q);const O=[];l[h]=O;const C=new Map(A.fields.filter(S=>S.fkTable!==void 0).map(S=>[S.name,kt(S,h,_,o)])),xe=Ft(A.fields,A.uniqueIndexes,h,R,k,u,y,S=>Et(C.get(S.name)(0)));for(let S=0;S<R;S+=1){const H=k+S,x={},$e=(E,Z)=>{if(Object.hasOwn(y,E)){const ce=ve(y[E],{field:E,index:H,row:x,store:l,table:h});if(ce!==void 0){x[E]=ce;return}}const ie=Z();ie!==void 0&&(x[E]=ie)};M===void 0?(x._id=F(h,H,x),q.push(x._id)):x._id=M[S];for(const E of A.fields){const Z=ye(f,h,H,E.name);$e(E.name,()=>xt(E,{allowOrphans:n,existingIds:o,idsByTable:_,index:H,input:Z,localIndex:S,now:u,pools:C,table:h,uniqueDeals:xe}))}O.push(x)}b.push({rows:O,table:h})}return b};export{Le as a,m as c,je as i,Vt as s};
|
|
@@ -34,6 +34,12 @@ type SeedOverrides = Record<string, Record<string, unknown>>;
|
|
|
34
34
|
/** Per-table row counts. */
|
|
35
35
|
type SeedCounts = Record<string, number>;
|
|
36
36
|
interface SeedOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Let a REQUIRED foreign key with no parent to point at take a fresh, dangling
|
|
39
|
+
* uuid instead of refusing. Off by default: a dangling id fails every join and
|
|
40
|
+
* `ctx.db.get` on the row, so it should be a deliberate choice.
|
|
41
|
+
*/
|
|
42
|
+
allowOrphans?: boolean;
|
|
37
43
|
/** Rows per table; falls back to {@link SeedOptions.defaultCount} when a table is absent. */
|
|
38
44
|
counts?: SeedCounts;
|
|
39
45
|
/** Count used for any selected table not present in `counts` (default `10`). */
|
|
@@ -52,6 +58,16 @@ interface SeedOptions {
|
|
|
52
58
|
* earlier batch.
|
|
53
59
|
*/
|
|
54
60
|
indexOffset?: Readonly<Record<string, number>>;
|
|
61
|
+
/**
|
|
62
|
+
* Wall-clock reference for time-valued columns (`createdAt`, `expiresAt`, …),
|
|
63
|
+
* as epoch-ms. Defaults to now.
|
|
64
|
+
*
|
|
65
|
+
* Pin it to make a plan byte-for-byte reproducible: seeding is deterministic
|
|
66
|
+
* in `seed` alone for every other column, and this is the one input that
|
|
67
|
+
* would otherwise drift between runs. A pinned `(seed, now)` pair is what
|
|
68
|
+
* makes a seeded screenshot, test fixture, or bug report replayable.
|
|
69
|
+
*/
|
|
70
|
+
now?: number;
|
|
55
71
|
/**
|
|
56
72
|
* Restrict seeding to these tables. Transitive `v.id(...)` parents are added
|
|
57
73
|
* automatically (unless already covered by `existingIds`) so child foreign
|
|
@@ -34,6 +34,12 @@ type SeedOverrides = Record<string, Record<string, unknown>>;
|
|
|
34
34
|
/** Per-table row counts. */
|
|
35
35
|
type SeedCounts = Record<string, number>;
|
|
36
36
|
interface SeedOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Let a REQUIRED foreign key with no parent to point at take a fresh, dangling
|
|
39
|
+
* uuid instead of refusing. Off by default: a dangling id fails every join and
|
|
40
|
+
* `ctx.db.get` on the row, so it should be a deliberate choice.
|
|
41
|
+
*/
|
|
42
|
+
allowOrphans?: boolean;
|
|
37
43
|
/** Rows per table; falls back to {@link SeedOptions.defaultCount} when a table is absent. */
|
|
38
44
|
counts?: SeedCounts;
|
|
39
45
|
/** Count used for any selected table not present in `counts` (default `10`). */
|
|
@@ -52,6 +58,16 @@ interface SeedOptions {
|
|
|
52
58
|
* earlier batch.
|
|
53
59
|
*/
|
|
54
60
|
indexOffset?: Readonly<Record<string, number>>;
|
|
61
|
+
/**
|
|
62
|
+
* Wall-clock reference for time-valued columns (`createdAt`, `expiresAt`, …),
|
|
63
|
+
* as epoch-ms. Defaults to now.
|
|
64
|
+
*
|
|
65
|
+
* Pin it to make a plan byte-for-byte reproducible: seeding is deterministic
|
|
66
|
+
* in `seed` alone for every other column, and this is the one input that
|
|
67
|
+
* would otherwise drift between runs. A pinned `(seed, now)` pair is what
|
|
68
|
+
* makes a seeded screenshot, test fixture, or bug report replayable.
|
|
69
|
+
*/
|
|
70
|
+
now?: number;
|
|
55
71
|
/**
|
|
56
72
|
* Restrict seeding to these tables. Transitive `v.id(...)` parents are added
|
|
57
73
|
* automatically (unless already covered by `existingIds`) so child foreign
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{s as a}from"./plan-BO3-4tGm.mjs";export{a as seedPlan};
|
package/dist/testing.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from '@lunora/server';
|
|
2
2
|
import { TestHarness } from '@lunora/testing';
|
|
3
|
-
import { a as SeedOptions } from "./packem_shared/plan.d-
|
|
3
|
+
import { a as SeedOptions } from "./packem_shared/plan.d-CpYSZG6V.mjs";
|
|
4
4
|
declare const seed: (harness: TestHarness, schema: Schema, options?: SeedOptions) => Promise<Record<string, string[]>>;
|
|
5
5
|
export { seed };
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from '@lunora/server';
|
|
2
2
|
import { TestHarness } from '@lunora/testing';
|
|
3
|
-
import { a as SeedOptions } from "./packem_shared/plan.d-
|
|
3
|
+
import { a as SeedOptions } from "./packem_shared/plan.d-CpYSZG6V.js";
|
|
4
4
|
declare const seed: (harness: TestHarness, schema: Schema, options?: SeedOptions) => Promise<Record<string, string[]>>;
|
|
5
5
|
export { seed };
|
package/dist/testing.mjs
CHANGED
|
@@ -1,53 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const reviveRow = (row, bigintFields, bytesFields) => {
|
|
4
|
-
if (bigintFields.size === 0 && bytesFields.size === 0) {
|
|
5
|
-
return row;
|
|
6
|
-
}
|
|
7
|
-
const revived = { ...row };
|
|
8
|
-
for (const field of bigintFields) {
|
|
9
|
-
const value = revived[field];
|
|
10
|
-
if (typeof value === "number") {
|
|
11
|
-
revived[field] = BigInt(value);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
for (const field of bytesFields) {
|
|
15
|
-
const value = revived[field];
|
|
16
|
-
if (Array.isArray(value)) {
|
|
17
|
-
revived[field] = Uint8Array.from(value).buffer;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
return revived;
|
|
21
|
-
};
|
|
22
|
-
const seed = async (harness, schema, options = {}) => {
|
|
23
|
-
const plan = seedPlan(schema, options);
|
|
24
|
-
const specs = introspectSchema(schema);
|
|
25
|
-
const bigintFieldsByTable = /* @__PURE__ */ new Map();
|
|
26
|
-
const bytesFieldsByTable = /* @__PURE__ */ new Map();
|
|
27
|
-
for (const spec of specs) {
|
|
28
|
-
const bigintFields = new Set(spec.fields.filter((field) => field.kind === "bigint").map((field) => field.name));
|
|
29
|
-
const bytesFields = new Set(spec.fields.filter((field) => field.kind === "bytes").map((field) => field.name));
|
|
30
|
-
if (bigintFields.size > 0) {
|
|
31
|
-
bigintFieldsByTable.set(spec.name, bigintFields);
|
|
32
|
-
}
|
|
33
|
-
if (bytesFields.size > 0) {
|
|
34
|
-
bytesFieldsByTable.set(spec.name, bytesFields);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const ids = {};
|
|
38
|
-
await harness.run(async (context) => {
|
|
39
|
-
const insert = context.db.insert;
|
|
40
|
-
for (const { rows, table } of plan) {
|
|
41
|
-
const tableIds = [];
|
|
42
|
-
const bigintFields = bigintFieldsByTable.get(table) ?? /* @__PURE__ */ new Set();
|
|
43
|
-
const bytesFields = bytesFieldsByTable.get(table) ?? /* @__PURE__ */ new Set();
|
|
44
|
-
for (const row of rows) {
|
|
45
|
-
tableIds.push(await insert(table, reviveRow(row, bigintFields, bytesFields), { allowExplicitId: true }));
|
|
46
|
-
}
|
|
47
|
-
ids[table] = tableIds;
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
return ids;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export { seed };
|
|
1
|
+
import{s as m,i as u}from"./packem_shared/plan-BO3-4tGm.mjs";const g=(r,o,a)=>{if(o.size===0&&a.size===0)return r;const s={...r};for(const n of o){const t=s[n];typeof t=="number"&&(s[n]=BigInt(t))}for(const n of a){const t=s[n];Array.isArray(t)&&(s[n]=Uint8Array.from(t).buffer)}return s},S=async(r,o,a={})=>{const s=m(o,a),n=u(o),t=new Map,l=new Map;for(const i of n){const c=new Set(i.fields.filter(e=>e.kind==="bigint").map(e=>e.name)),f=new Set(i.fields.filter(e=>e.kind==="bytes").map(e=>e.name));c.size>0&&t.set(i.name,c),f.size>0&&l.set(i.name,f)}const d={};return await r.run(async i=>{const c=i.db.insert;for(const{rows:f,table:e}of s){const b=[],p=t.get(e)??new Set,w=l.get(e)??new Set;for(const y of f)b.push(await c(e,g(y,p,w),{allowExplicitId:!0}));d[e]=b}}),d};export{S as seed};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/seed",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.271",
|
|
4
4
|
"description": "Schema-driven, deterministic database seeding for Lunora: realistic fake data from defineSchema",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -47,11 +47,12 @@
|
|
|
47
47
|
"access": "public"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@faker-js/faker": "
|
|
51
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
50
|
+
"@faker-js/faker": "10.5.0",
|
|
51
|
+
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@lunora/server": ">=1.0.0-alpha.24 <2.0.0-0",
|
|
55
|
+
"@lunora/testing": ">=1.0.0-alpha.131 <2.0.0-0",
|
|
55
56
|
"@lunora/values": ">=1.0.0-alpha.7 <2.0.0-0"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { a as setHashKey, s as seedPlan, c as copycat } from './plan-DybYaa3F.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 runSeedTable = 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
|
-
let queue = Promise.resolve();
|
|
68
|
-
const seedTable = (table, spec, callOptions) => {
|
|
69
|
-
const result = queue.then(() => runSeedTable(table, spec, callOptions));
|
|
70
|
-
queue = result.then(
|
|
71
|
-
() => void 0,
|
|
72
|
-
() => void 0
|
|
73
|
-
);
|
|
74
|
-
return result;
|
|
75
|
-
};
|
|
76
|
-
const state = {
|
|
77
|
-
$ids: idsByTable,
|
|
78
|
-
$reset: () => {
|
|
79
|
-
for (const key of Object.keys(store)) {
|
|
80
|
-
delete store[key];
|
|
81
|
-
}
|
|
82
|
-
for (const key of Object.keys(idsByTable)) {
|
|
83
|
-
delete idsByTable[key];
|
|
84
|
-
}
|
|
85
|
-
for (const key of Object.keys(createdCount)) {
|
|
86
|
-
delete createdCount[key];
|
|
87
|
-
}
|
|
88
|
-
},
|
|
89
|
-
$store: store
|
|
90
|
-
};
|
|
91
|
-
return /* @__PURE__ */ new Proxy(state, {
|
|
92
|
-
get(target, property, receiver) {
|
|
93
|
-
if (typeof property === "string" && !property.startsWith("$") && Object.hasOwn(schema.tables, property)) {
|
|
94
|
-
return (spec, callOptions) => seedTable(property, spec, callOptions);
|
|
95
|
-
}
|
|
96
|
-
return Reflect.get(target, property, receiver);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export { createSeedClient };
|
|
@@ -1,526 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { faker } from '@faker-js/faker';
|
|
3
|
-
import { optionalInner } from '@lunora/values';
|
|
4
|
-
|
|
5
|
-
let hashSalt = 0;
|
|
6
|
-
const compareStrings = (a, b) => {
|
|
7
|
-
if (a < b) {
|
|
8
|
-
return -1;
|
|
9
|
-
}
|
|
10
|
-
if (a > b) {
|
|
11
|
-
return 1;
|
|
12
|
-
}
|
|
13
|
-
return 0;
|
|
14
|
-
};
|
|
15
|
-
const stableStringify = (input) => {
|
|
16
|
-
if (input === void 0) {
|
|
17
|
-
return "undefined";
|
|
18
|
-
}
|
|
19
|
-
if (typeof input === "bigint") {
|
|
20
|
-
return `${input.toString()}n`;
|
|
21
|
-
}
|
|
22
|
-
if (input === null || typeof input !== "object") {
|
|
23
|
-
return JSON.stringify(input) ?? "null";
|
|
24
|
-
}
|
|
25
|
-
if (Array.isArray(input)) {
|
|
26
|
-
return `[${input.map((item) => stableStringify(item)).join(",")}]`;
|
|
27
|
-
}
|
|
28
|
-
const entries = Object.keys(input).toSorted(compareStrings).map((key) => `${JSON.stringify(key)}:${stableStringify(input[key])}`);
|
|
29
|
-
return `{${entries.join(",")}}`;
|
|
30
|
-
};
|
|
31
|
-
const cyrb53 = (text, seed) => {
|
|
32
|
-
let h1 = 3735928559 ^ seed;
|
|
33
|
-
let h2 = 1103547991 ^ seed;
|
|
34
|
-
for (let index = 0; index < text.length; index += 1) {
|
|
35
|
-
const ch = text.codePointAt(index) ?? 0;
|
|
36
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
37
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
38
|
-
}
|
|
39
|
-
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
40
|
-
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
41
|
-
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
42
|
-
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
43
|
-
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
44
|
-
};
|
|
45
|
-
const STRING_DOMAIN_TAG = "\0";
|
|
46
|
-
const hashInput = (input) => {
|
|
47
|
-
const text = typeof input === "string" ? STRING_DOMAIN_TAG + input : stableStringify(input);
|
|
48
|
-
return cyrb53(text, hashSalt) % 4294967296;
|
|
49
|
-
};
|
|
50
|
-
const setHashKey = (key) => {
|
|
51
|
-
if (typeof key === "number") {
|
|
52
|
-
hashSalt = key >>> 0;
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (typeof key === "string") {
|
|
56
|
-
hashSalt = cyrb53(key, 0) % 4294967296;
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
let folded = 0;
|
|
60
|
-
for (const word of key) {
|
|
61
|
-
folded = Math.imul(folded ^ word, 2654435761) >>> 0;
|
|
62
|
-
}
|
|
63
|
-
hashSalt = folded;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const seeded = (input, produce) => {
|
|
67
|
-
faker.seed(hashInput(input));
|
|
68
|
-
return produce();
|
|
69
|
-
};
|
|
70
|
-
const resolveCount = (input, range) => {
|
|
71
|
-
if (typeof range === "number") {
|
|
72
|
-
return range;
|
|
73
|
-
}
|
|
74
|
-
const [min, max] = range;
|
|
75
|
-
return seeded(["__count__", input], () => faker.number.int({ max, min }));
|
|
76
|
-
};
|
|
77
|
-
const capitalizeWord = (word) => word.length === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1);
|
|
78
|
-
const LOWER_ALPHA = /[a-z]/;
|
|
79
|
-
const UPPER_ALPHA = /[A-Z]/;
|
|
80
|
-
const DIGIT = /\d/;
|
|
81
|
-
const copycat = {
|
|
82
|
-
bool(input) {
|
|
83
|
-
return seeded(input, () => faker.datatype.boolean());
|
|
84
|
-
},
|
|
85
|
-
char(input) {
|
|
86
|
-
return seeded(input, () => faker.string.alpha(1));
|
|
87
|
-
},
|
|
88
|
-
city(input) {
|
|
89
|
-
return seeded(input, () => faker.location.city());
|
|
90
|
-
},
|
|
91
|
-
country(input) {
|
|
92
|
-
return seeded(input, () => faker.location.country());
|
|
93
|
-
},
|
|
94
|
-
countryCode(input) {
|
|
95
|
-
return seeded(input, () => faker.location.countryCode());
|
|
96
|
-
},
|
|
97
|
-
/** ISO-8601 date string between `min`/`max` (default years 1980–2020). */
|
|
98
|
-
dateString(input, options) {
|
|
99
|
-
const min = options?.min ?? /* @__PURE__ */ new Date("1980-01-01T00:00:00.000Z");
|
|
100
|
-
const max = options?.max ?? /* @__PURE__ */ new Date("2020-01-01T00:00:00.000Z");
|
|
101
|
-
return seeded(input, () => faker.date.between({ from: min, to: max }).toISOString());
|
|
102
|
-
},
|
|
103
|
-
digit(input) {
|
|
104
|
-
return seeded(input, () => String(faker.number.int({ max: 9, min: 0 })));
|
|
105
|
-
},
|
|
106
|
-
email(input) {
|
|
107
|
-
return seeded(input, () => faker.internet.email().toLowerCase());
|
|
108
|
-
},
|
|
109
|
-
firstName(input) {
|
|
110
|
-
return seeded(input, () => faker.person.firstName());
|
|
111
|
-
},
|
|
112
|
-
float(input, options) {
|
|
113
|
-
const { fractionDigits = 2, max = 1e3, min = 0 } = options ?? {};
|
|
114
|
-
return seeded(input, () => faker.number.float({ fractionDigits, max, min }));
|
|
115
|
-
},
|
|
116
|
-
fullName(input) {
|
|
117
|
-
return seeded(input, () => faker.person.fullName());
|
|
118
|
-
},
|
|
119
|
-
hex(input) {
|
|
120
|
-
return seeded(input, () => faker.string.hexadecimal({ casing: "lower", length: 1, prefix: "" }));
|
|
121
|
-
},
|
|
122
|
-
int(input, options) {
|
|
123
|
-
const { max = 1e3, min = 0 } = options ?? {};
|
|
124
|
-
return seeded(input, () => faker.number.int({ max, min }));
|
|
125
|
-
},
|
|
126
|
-
ipv4(input) {
|
|
127
|
-
return seeded(input, () => faker.internet.ipv4());
|
|
128
|
-
},
|
|
129
|
-
lastName(input) {
|
|
130
|
-
return seeded(input, () => faker.person.lastName());
|
|
131
|
-
},
|
|
132
|
-
mac(input) {
|
|
133
|
-
return seeded(input, () => faker.internet.mac());
|
|
134
|
-
},
|
|
135
|
-
/** Pick the array element corresponding to `input`. Returns `undefined` for an empty array. */
|
|
136
|
-
oneOf(input, values) {
|
|
137
|
-
if (values.length === 0) {
|
|
138
|
-
return void 0;
|
|
139
|
-
}
|
|
140
|
-
return seeded(input, () => faker.helpers.arrayElement(values));
|
|
141
|
-
},
|
|
142
|
-
paragraph(input) {
|
|
143
|
-
return seeded(input, () => faker.lorem.paragraph());
|
|
144
|
-
},
|
|
145
|
-
password(input) {
|
|
146
|
-
return seeded(input, () => faker.internet.password());
|
|
147
|
-
},
|
|
148
|
-
phoneNumber(input) {
|
|
149
|
-
return seeded(input, () => faker.phone.number());
|
|
150
|
-
},
|
|
151
|
-
/**
|
|
152
|
-
* Scramble a string in place: letters become seeded letters (case
|
|
153
|
-
* preserved), digits become seeded digits, every other character is kept.
|
|
154
|
-
* Length is preserved. Characters listed in `preserve` pass through untouched.
|
|
155
|
-
*/
|
|
156
|
-
scramble(input, options) {
|
|
157
|
-
const preserve = new Set(options?.preserve);
|
|
158
|
-
return Array.from({ length: input.length }, (_unused, index) => {
|
|
159
|
-
const char = input.charAt(index);
|
|
160
|
-
if (preserve.has(char)) {
|
|
161
|
-
return char;
|
|
162
|
-
}
|
|
163
|
-
if (LOWER_ALPHA.test(char)) {
|
|
164
|
-
return seeded([input, index, "l"], () => faker.string.alpha({ casing: "lower", length: 1 }));
|
|
165
|
-
}
|
|
166
|
-
if (UPPER_ALPHA.test(char)) {
|
|
167
|
-
return seeded([input, index, "u"], () => faker.string.alpha({ casing: "upper", length: 1 }));
|
|
168
|
-
}
|
|
169
|
-
if (DIGIT.test(char)) {
|
|
170
|
-
return seeded([input, index, "d"], () => String(faker.number.int({ max: 9, min: 0 })));
|
|
171
|
-
}
|
|
172
|
-
return char;
|
|
173
|
-
}).join("");
|
|
174
|
-
},
|
|
175
|
-
sentence(input, options) {
|
|
176
|
-
return seeded(input, () => faker.lorem.sentence(options ? { max: options.max ?? 8, min: options.min ?? 3 } : void 0));
|
|
177
|
-
},
|
|
178
|
-
slug(input) {
|
|
179
|
-
return seeded(input, () => faker.lorem.slug());
|
|
180
|
-
},
|
|
181
|
-
/** Pick a deterministic subset (size within `range`) of `values`, no repeats. */
|
|
182
|
-
someOf(input, range, values) {
|
|
183
|
-
const count = Math.min(resolveCount(["__some__", input], range), values.length);
|
|
184
|
-
return seeded(input, () => faker.helpers.arrayElements(values, count));
|
|
185
|
-
},
|
|
186
|
-
streetAddress(input) {
|
|
187
|
-
return seeded(input, () => faker.location.streetAddress());
|
|
188
|
-
},
|
|
189
|
-
streetName(input) {
|
|
190
|
-
return seeded(input, () => faker.location.street());
|
|
191
|
-
},
|
|
192
|
-
timezone(input) {
|
|
193
|
-
return seeded(input, () => faker.location.timeZone());
|
|
194
|
-
},
|
|
195
|
-
/**
|
|
196
|
-
* Call `produce` once per element for a deterministic count within `range`,
|
|
197
|
-
* passing each a distinct sub-input so the elements differ but stay stable.
|
|
198
|
-
*/
|
|
199
|
-
times(input, range, produce) {
|
|
200
|
-
const count = resolveCount(input, range);
|
|
201
|
-
const out = [];
|
|
202
|
-
for (let index = 0; index < count; index += 1) {
|
|
203
|
-
out.push(produce([input, index]));
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
206
|
-
},
|
|
207
|
-
url(input) {
|
|
208
|
-
return seeded(input, () => faker.internet.url());
|
|
209
|
-
},
|
|
210
|
-
username(input) {
|
|
211
|
-
return seeded(input, () => faker.internet.username());
|
|
212
|
-
},
|
|
213
|
-
uuid(input) {
|
|
214
|
-
return seeded(input, () => faker.string.uuid());
|
|
215
|
-
},
|
|
216
|
-
word(input, options) {
|
|
217
|
-
const word = seeded(input, () => faker.lorem.word());
|
|
218
|
-
return options?.capitalize === true ? capitalizeWord(word) : word;
|
|
219
|
-
},
|
|
220
|
-
words(input, options) {
|
|
221
|
-
return seeded(input, () => faker.lorem.words(options ? { max: options.max ?? 5, min: options.min ?? 2 } : void 0));
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const metaOf = (validator) => validator._meta ?? {};
|
|
226
|
-
const unwrapOptional = (validator) => optionalInner(validator) ?? validator;
|
|
227
|
-
const hasServerDefault = (validator) => {
|
|
228
|
-
const { column } = metaOf(validator);
|
|
229
|
-
if (column?.defaultValue !== void 0 || column?.defaultFn !== void 0) {
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
232
|
-
const inner = optionalInner(validator);
|
|
233
|
-
return inner === void 0 ? false : hasServerDefault(inner);
|
|
234
|
-
};
|
|
235
|
-
const describeField = (name, validator) => {
|
|
236
|
-
const inner = unwrapOptional(validator);
|
|
237
|
-
const meta = metaOf(inner);
|
|
238
|
-
return {
|
|
239
|
-
fkTable: inner.kind === "id" ? meta.tableName : void 0,
|
|
240
|
-
hasServerDefault: hasServerDefault(validator),
|
|
241
|
-
kind: inner.kind,
|
|
242
|
-
name,
|
|
243
|
-
// `notNull` is tri-state: `true` (default / `.notNull()`), `false`
|
|
244
|
-
// (`.nullable()`), or `undefined` (no column metadata). Only an explicit
|
|
245
|
-
// `false` means the user opted the column into SQL `NULL` — which is what
|
|
246
|
-
// `fkFallback` keys off to emit `null` for an unresolved nullable FK. This
|
|
247
|
-
// mirrors codegen's own `isNullable` (`column?.notNull === false`).
|
|
248
|
-
nullable: meta.column?.notNull === false,
|
|
249
|
-
optional: validator.kind === "optional",
|
|
250
|
-
validator: inner
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
const introspectSchema = (schema) => {
|
|
254
|
-
const { tables } = schema;
|
|
255
|
-
return Object.entries(tables).map(([name, table]) => {
|
|
256
|
-
return {
|
|
257
|
-
fields: Object.entries(table.shape).map(([fieldName, validator]) => describeField(fieldName, validator)),
|
|
258
|
-
name
|
|
259
|
-
};
|
|
260
|
-
});
|
|
261
|
-
};
|
|
262
|
-
const orderTables = (specs, selected) => {
|
|
263
|
-
const byName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
264
|
-
const parentsOf = (name) => {
|
|
265
|
-
const spec = byName.get(name);
|
|
266
|
-
if (spec === void 0) {
|
|
267
|
-
return /* @__PURE__ */ new Set();
|
|
268
|
-
}
|
|
269
|
-
const parents = /* @__PURE__ */ new Set();
|
|
270
|
-
for (const field of spec.fields) {
|
|
271
|
-
if (field.fkTable !== void 0 && field.fkTable !== name && selected.has(field.fkTable)) {
|
|
272
|
-
parents.add(field.fkTable);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return parents;
|
|
276
|
-
};
|
|
277
|
-
const ordered = [];
|
|
278
|
-
const placed = /* @__PURE__ */ new Set();
|
|
279
|
-
const pending = [...selected].filter((name) => byName.has(name));
|
|
280
|
-
while (pending.length > 0) {
|
|
281
|
-
const readyIndex = pending.findIndex((name2) => [...parentsOf(name2)].every((parent) => placed.has(parent)));
|
|
282
|
-
const index = readyIndex === -1 ? 0 : readyIndex;
|
|
283
|
-
const [name] = pending.splice(index, 1);
|
|
284
|
-
if (name === void 0) {
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
ordered.push(name);
|
|
288
|
-
placed.add(name);
|
|
289
|
-
}
|
|
290
|
-
return ordered;
|
|
291
|
-
};
|
|
292
|
-
const fkParentClosure = (specs, roots, stopAt = /* @__PURE__ */ new Set()) => {
|
|
293
|
-
const byName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
294
|
-
const result = new Set(roots);
|
|
295
|
-
const stack = [...result];
|
|
296
|
-
while (stack.length > 0) {
|
|
297
|
-
const name = stack.pop();
|
|
298
|
-
if (name === void 0) {
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
const spec = byName.get(name);
|
|
302
|
-
if (spec === void 0) {
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
if (stopAt.has(name)) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
for (const field of spec.fields) {
|
|
309
|
-
if (field.fkTable !== void 0 && field.fkTable !== name && byName.has(field.fkTable) && !result.has(field.fkTable)) {
|
|
310
|
-
result.add(field.fkTable);
|
|
311
|
-
stack.push(field.fkTable);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return result;
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
const constraintsOf = (validator) => metaOf(validator).constraints ?? {};
|
|
319
|
-
const STRING_HEURISTICS = [
|
|
320
|
-
{ generate: (input) => copycat.email(input), keywords: ["email"] },
|
|
321
|
-
{ generate: (input) => copycat.firstName(input), keywords: ["firstname"] },
|
|
322
|
-
{ generate: (input) => copycat.lastName(input), keywords: ["lastname", "surname"] },
|
|
323
|
-
{ generate: (input) => copycat.username(input), keywords: ["username"] },
|
|
324
|
-
{ generate: (input) => copycat.fullName(input), keywords: ["name"] },
|
|
325
|
-
{ generate: (input) => copycat.sentence(input, { max: 5, min: 2 }), keywords: ["title"] },
|
|
326
|
-
{ generate: (input) => copycat.url(input), keywords: ["url", "link", "image", "avatar"] },
|
|
327
|
-
{ generate: (input) => copycat.phoneNumber(input), keywords: ["phone"] },
|
|
328
|
-
{ generate: (input) => copycat.paragraph(input), keywords: ["description", "bio", "body", "content", "text"] },
|
|
329
|
-
{ generate: (input) => copycat.slug(input), keywords: ["slug", "key", "code"] },
|
|
330
|
-
{ generate: (input) => copycat.city(input), keywords: ["city"] },
|
|
331
|
-
{ generate: (input) => copycat.country(input), keywords: ["country"] },
|
|
332
|
-
{ generate: (input) => copycat.streetAddress(input), keywords: ["address", "street"] },
|
|
333
|
-
{ generate: (input) => copycat.password(input), keywords: ["password", "secret", "token"] }
|
|
334
|
-
];
|
|
335
|
-
const generateString = (fieldName, input, constraints) => {
|
|
336
|
-
const lower = fieldName.toLowerCase();
|
|
337
|
-
const rule = STRING_HEURISTICS.find((entry) => entry.keywords.some((keyword) => lower.includes(keyword)));
|
|
338
|
-
const value = rule === void 0 ? copycat.word(input) : rule.generate(input);
|
|
339
|
-
const { maxLength, minLength } = constraints;
|
|
340
|
-
if (maxLength !== void 0 && minLength !== void 0 && minLength > maxLength) {
|
|
341
|
-
throw new LunoraError(
|
|
342
|
-
"INTERNAL",
|
|
343
|
-
`Seed constraint error for field "${fieldName}": minLength (${String(minLength)}) > maxLength (${String(maxLength)}). Adjust the schema constraints.`
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
const truncated = maxLength !== void 0 && value.length > maxLength ? value.slice(0, maxLength) : value;
|
|
347
|
-
if (minLength !== void 0 && truncated.length < minLength) {
|
|
348
|
-
return truncated.padEnd(minLength, truncated.length > 0 ? truncated : "x");
|
|
349
|
-
}
|
|
350
|
-
return truncated;
|
|
351
|
-
};
|
|
352
|
-
const generateValue = (validator, fieldName, input) => {
|
|
353
|
-
const inner = unwrapOptional(validator);
|
|
354
|
-
const constraints = constraintsOf(inner);
|
|
355
|
-
if (constraints.enum !== void 0 && constraints.enum.length > 0) {
|
|
356
|
-
return copycat.oneOf(input, constraints.enum);
|
|
357
|
-
}
|
|
358
|
-
switch (inner.kind) {
|
|
359
|
-
case "any": {
|
|
360
|
-
return copycat.word(input);
|
|
361
|
-
}
|
|
362
|
-
case "array": {
|
|
363
|
-
const element = metaOf(inner).inner;
|
|
364
|
-
if (element === void 0) {
|
|
365
|
-
return [];
|
|
366
|
-
}
|
|
367
|
-
return copycat.times(input, [1, 3], (itemInput) => generateValue(element, fieldName, itemInput));
|
|
368
|
-
}
|
|
369
|
-
case "bigint": {
|
|
370
|
-
return copycat.int(input, { max: 1e6, min: 0 });
|
|
371
|
-
}
|
|
372
|
-
case "boolean": {
|
|
373
|
-
return copycat.bool(input);
|
|
374
|
-
}
|
|
375
|
-
case "bytes": {
|
|
376
|
-
return Array.from({ length: 8 }, (_, index) => copycat.int([input, index], { max: 255, min: 0 }));
|
|
377
|
-
}
|
|
378
|
-
case "date":
|
|
379
|
-
case "timestamp": {
|
|
380
|
-
return new Date(copycat.dateString(input)).getTime();
|
|
381
|
-
}
|
|
382
|
-
case "id": {
|
|
383
|
-
return copycat.uuid(input);
|
|
384
|
-
}
|
|
385
|
-
case "literal": {
|
|
386
|
-
return metaOf(inner).value;
|
|
387
|
-
}
|
|
388
|
-
case "null": {
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
case "number": {
|
|
392
|
-
const { maximum, minimum } = constraints;
|
|
393
|
-
if (maximum !== void 0 && minimum !== void 0 && minimum > maximum) {
|
|
394
|
-
throw new LunoraError(
|
|
395
|
-
"INTERNAL",
|
|
396
|
-
`Seed constraint error for field "${fieldName}": minimum (${String(minimum)}) > maximum (${String(maximum)}). Adjust the schema constraints.`
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
const min = minimum ?? 0;
|
|
400
|
-
const max = maximum ?? 1e3;
|
|
401
|
-
if (!Number.isInteger(min) || !Number.isInteger(max)) {
|
|
402
|
-
return copycat.float(input, { max, min });
|
|
403
|
-
}
|
|
404
|
-
return copycat.int(input, { max, min });
|
|
405
|
-
}
|
|
406
|
-
case "object": {
|
|
407
|
-
const shape = metaOf(inner).shape ?? {};
|
|
408
|
-
return Object.fromEntries(Object.entries(shape).map(([key, child]) => [key, generateValue(child, key, [input, key])]));
|
|
409
|
-
}
|
|
410
|
-
case "record": {
|
|
411
|
-
const { keyValidator, valueValidator } = metaOf(inner);
|
|
412
|
-
const entries = copycat.times(input, [1, 3], (itemInput) => {
|
|
413
|
-
const key = keyValidator === void 0 ? copycat.word(["k", itemInput]) : String(generateValue(keyValidator, fieldName, ["k", itemInput]));
|
|
414
|
-
const value = valueValidator === void 0 ? copycat.word(["v", itemInput]) : generateValue(valueValidator, fieldName, ["v", itemInput]);
|
|
415
|
-
return [key, value];
|
|
416
|
-
});
|
|
417
|
-
return Object.fromEntries(entries);
|
|
418
|
-
}
|
|
419
|
-
case "storage": {
|
|
420
|
-
return `seed/${copycat.uuid(input)}`;
|
|
421
|
-
}
|
|
422
|
-
case "string": {
|
|
423
|
-
return generateString(fieldName, input, constraints);
|
|
424
|
-
}
|
|
425
|
-
case "union": {
|
|
426
|
-
const members = metaOf(inner).members ?? [];
|
|
427
|
-
const chosen = copycat.oneOf(input, members);
|
|
428
|
-
return chosen === void 0 ? copycat.word(input) : generateValue(chosen, fieldName, [input, "u"]);
|
|
429
|
-
}
|
|
430
|
-
default: {
|
|
431
|
-
return copycat.word(input);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
const resolveOverride = (override, context) => typeof override === "function" ? override(context) : override;
|
|
437
|
-
const fkFallback = (field, input) => {
|
|
438
|
-
if (field.optional) {
|
|
439
|
-
return void 0;
|
|
440
|
-
}
|
|
441
|
-
return field.nullable ? null : copycat.uuid(input);
|
|
442
|
-
};
|
|
443
|
-
const fkPool = (field, table, localIndex, idsByTable, existingIds) => {
|
|
444
|
-
const fkTable = field.fkTable;
|
|
445
|
-
const seeded = fkTable === table ? (idsByTable.get(table) ?? []).slice(0, localIndex) : idsByTable.get(fkTable) ?? [];
|
|
446
|
-
const existing = existingIds[fkTable] ?? [];
|
|
447
|
-
return existing.length === 0 ? seeded : [...seeded, ...existing];
|
|
448
|
-
};
|
|
449
|
-
const cellInput = (seed, table, index, column) => [seed, table, index, column];
|
|
450
|
-
const generateField = (field, table, input, localIndex, idsByTable, existingIds) => {
|
|
451
|
-
if (field.fkTable !== void 0) {
|
|
452
|
-
const pool = fkPool(field, table, localIndex, idsByTable, existingIds);
|
|
453
|
-
if (pool.length === 0) {
|
|
454
|
-
return fkFallback(field, input);
|
|
455
|
-
}
|
|
456
|
-
return copycat.oneOf(input, pool);
|
|
457
|
-
}
|
|
458
|
-
if (field.hasServerDefault) {
|
|
459
|
-
return void 0;
|
|
460
|
-
}
|
|
461
|
-
return generateValue(field.validator, field.name, input);
|
|
462
|
-
};
|
|
463
|
-
const seedPlan = (schema, options = {}) => {
|
|
464
|
-
const { counts = {}, defaultCount = 10, existingIds = {}, indexOffset = {}, only, overrides = {}, seed = 0 } = options;
|
|
465
|
-
setHashKey(seed);
|
|
466
|
-
const specs = introspectSchema(schema);
|
|
467
|
-
if (only !== void 0) {
|
|
468
|
-
const known = new Set(specs.map((spec) => spec.name));
|
|
469
|
-
for (const name of only) {
|
|
470
|
-
if (!known.has(name)) {
|
|
471
|
-
const available = specs.map((spec) => spec.name).join(", ");
|
|
472
|
-
throw new LunoraError("BAD_REQUEST", `unknown table "${name}" in seed \`only\` — schema defines: ${available || "(no tables)"}`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
const requested = new Set(only ?? specs.map((spec) => spec.name));
|
|
477
|
-
const covered = new Set(Object.keys(existingIds).filter((table) => !requested.has(table) && (existingIds[table] ?? []).length > 0));
|
|
478
|
-
const selected = new Set(
|
|
479
|
-
[...fkParentClosure(specs, requested, covered)].filter((table) => requested.has(table) || (existingIds[table] ?? []).length === 0)
|
|
480
|
-
);
|
|
481
|
-
const order = orderTables(specs, selected);
|
|
482
|
-
const specByName = new Map(specs.map((spec) => [spec.name, spec]));
|
|
483
|
-
const idsByTable = /* @__PURE__ */ new Map();
|
|
484
|
-
const storeRows = {};
|
|
485
|
-
const plan = [];
|
|
486
|
-
for (const table of order) {
|
|
487
|
-
const spec = specByName.get(table);
|
|
488
|
-
if (spec === void 0) {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
const tableOverrides = overrides[table] ?? {};
|
|
492
|
-
const count = counts[table] ?? defaultCount;
|
|
493
|
-
const offset = indexOffset[table] ?? 0;
|
|
494
|
-
const ids = [];
|
|
495
|
-
idsByTable.set(table, ids);
|
|
496
|
-
const rows = [];
|
|
497
|
-
storeRows[table] = rows;
|
|
498
|
-
for (let localIndex = 0; localIndex < count; localIndex += 1) {
|
|
499
|
-
const index = offset + localIndex;
|
|
500
|
-
const row = {};
|
|
501
|
-
const apply = (field, fallback) => {
|
|
502
|
-
if (Object.hasOwn(tableOverrides, field)) {
|
|
503
|
-
const overridden = resolveOverride(tableOverrides[field], { field, index, row, store: storeRows, table });
|
|
504
|
-
if (overridden !== void 0) {
|
|
505
|
-
row[field] = overridden;
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const value = fallback();
|
|
510
|
-
if (value !== void 0) {
|
|
511
|
-
row[field] = value;
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
apply("_id", () => copycat.uuid(cellInput(seed, table, index, "_id")));
|
|
515
|
-
ids.push(row._id);
|
|
516
|
-
for (const field of spec.fields) {
|
|
517
|
-
apply(field.name, () => generateField(field, table, cellInput(seed, table, index, field.name), localIndex, idsByTable, existingIds));
|
|
518
|
-
}
|
|
519
|
-
rows.push(row);
|
|
520
|
-
}
|
|
521
|
-
plan.push({ rows, table });
|
|
522
|
-
}
|
|
523
|
-
return plan;
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
export { setHashKey as a, copycat as c, introspectSchema as i, seedPlan as s };
|