velocious 1.0.552 → 1.0.555

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.
Files changed (117) hide show
  1. package/README.md +2 -0
  2. package/build/background-jobs/status-reporter.js +15 -2
  3. package/build/background-jobs/worker.js +5 -1
  4. package/build/frontend-models/base.js +105 -47
  5. package/build/frontend-models/transport-deadline.js +66 -0
  6. package/build/src/background-jobs/status-reporter.d.ts +3 -1
  7. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  8. package/build/src/background-jobs/status-reporter.js +16 -3
  9. package/build/src/background-jobs/worker.d.ts.map +1 -1
  10. package/build/src/background-jobs/worker.js +6 -2
  11. package/build/src/frontend-models/base.d.ts +10 -1
  12. package/build/src/frontend-models/base.d.ts.map +1 -1
  13. package/build/src/frontend-models/base.js +71 -26
  14. package/build/src/frontend-models/transport-deadline.d.ts +26 -0
  15. package/build/src/frontend-models/transport-deadline.d.ts.map +1 -0
  16. package/build/src/frontend-models/transport-deadline.js +61 -0
  17. package/build/src/testing/factory/association-declaration.d.ts +37 -0
  18. package/build/src/testing/factory/association-declaration.d.ts.map +1 -0
  19. package/build/src/testing/factory/association-declaration.js +41 -0
  20. package/build/src/testing/factory/declarations.d.ts +190 -0
  21. package/build/src/testing/factory/declarations.d.ts.map +1 -0
  22. package/build/src/testing/factory/declarations.js +98 -0
  23. package/build/src/testing/factory/definition-builder.d.ts +132 -0
  24. package/build/src/testing/factory/definition-builder.d.ts.map +1 -0
  25. package/build/src/testing/factory/definition-builder.js +417 -0
  26. package/build/src/testing/factory/errors.d.ts +62 -0
  27. package/build/src/testing/factory/errors.d.ts.map +1 -0
  28. package/build/src/testing/factory/errors.js +67 -0
  29. package/build/src/testing/factory/evaluation-context.d.ts +95 -0
  30. package/build/src/testing/factory/evaluation-context.d.ts.map +1 -0
  31. package/build/src/testing/factory/evaluation-context.js +175 -0
  32. package/build/src/testing/factory/events.d.ts +21 -0
  33. package/build/src/testing/factory/events.d.ts.map +1 -0
  34. package/build/src/testing/factory/events.js +26 -0
  35. package/build/src/testing/factory/factory-definition.d.ts +39 -0
  36. package/build/src/testing/factory/factory-definition.d.ts.map +1 -0
  37. package/build/src/testing/factory/factory-definition.js +35 -0
  38. package/build/src/testing/factory/factory-registry.d.ts +263 -0
  39. package/build/src/testing/factory/factory-registry.d.ts.map +1 -0
  40. package/build/src/testing/factory/factory-registry.js +438 -0
  41. package/build/src/testing/factory/factory-runner.d.ts +179 -0
  42. package/build/src/testing/factory/factory-runner.d.ts.map +1 -0
  43. package/build/src/testing/factory/factory-runner.js +266 -0
  44. package/build/src/testing/factory/index.d.ts +18 -0
  45. package/build/src/testing/factory/index.d.ts.map +1 -0
  46. package/build/src/testing/factory/index.js +21 -0
  47. package/build/src/testing/factory/linter.d.ts +55 -0
  48. package/build/src/testing/factory/linter.d.ts.map +1 -0
  49. package/build/src/testing/factory/linter.js +106 -0
  50. package/build/src/testing/factory/model-contract.d.ts +11 -0
  51. package/build/src/testing/factory/model-contract.d.ts.map +1 -0
  52. package/build/src/testing/factory/model-contract.js +26 -0
  53. package/build/src/testing/factory/node/load-definitions.d.ts +24 -0
  54. package/build/src/testing/factory/node/load-definitions.d.ts.map +1 -0
  55. package/build/src/testing/factory/node/load-definitions.js +87 -0
  56. package/build/src/testing/factory/sequence.d.ts +73 -0
  57. package/build/src/testing/factory/sequence.d.ts.map +1 -0
  58. package/build/src/testing/factory/sequence.js +78 -0
  59. package/build/src/testing/factory/strategies/attributes-for.d.ts +21 -0
  60. package/build/src/testing/factory/strategies/attributes-for.d.ts.map +1 -0
  61. package/build/src/testing/factory/strategies/attributes-for.js +22 -0
  62. package/build/src/testing/factory/strategies/base.d.ts +101 -0
  63. package/build/src/testing/factory/strategies/base.d.ts.map +1 -0
  64. package/build/src/testing/factory/strategies/base.js +191 -0
  65. package/build/src/testing/factory/strategies/build.d.ts +20 -0
  66. package/build/src/testing/factory/strategies/build.d.ts.map +1 -0
  67. package/build/src/testing/factory/strategies/build.js +34 -0
  68. package/build/src/testing/factory/strategies/create.d.ts +32 -0
  69. package/build/src/testing/factory/strategies/create.d.ts.map +1 -0
  70. package/build/src/testing/factory/strategies/create.js +57 -0
  71. package/build/src/testing/factory/trait-definition.d.ts +22 -0
  72. package/build/src/testing/factory/trait-definition.d.ts.map +1 -0
  73. package/build/src/testing/factory/trait-definition.js +22 -0
  74. package/build/testing/factory/association-declaration.js +50 -0
  75. package/build/testing/factory/declarations.js +110 -0
  76. package/build/testing/factory/definition-builder.js +476 -0
  77. package/build/testing/factory/errors.js +79 -0
  78. package/build/testing/factory/evaluation-context.js +199 -0
  79. package/build/testing/factory/events.js +30 -0
  80. package/build/testing/factory/factory-definition.js +41 -0
  81. package/build/testing/factory/factory-registry.js +501 -0
  82. package/build/testing/factory/factory-runner.js +296 -0
  83. package/build/testing/factory/index.js +24 -0
  84. package/build/testing/factory/linter.js +119 -0
  85. package/build/testing/factory/model-contract.js +31 -0
  86. package/build/testing/factory/node/load-definitions.js +103 -0
  87. package/build/testing/factory/sequence.js +91 -0
  88. package/build/testing/factory/strategies/attributes-for.js +24 -0
  89. package/build/testing/factory/strategies/base.js +209 -0
  90. package/build/testing/factory/strategies/build.js +42 -0
  91. package/build/testing/factory/strategies/create.js +68 -0
  92. package/build/testing/factory/trait-definition.js +24 -0
  93. package/build/tsconfig.tsbuildinfo +1 -1
  94. package/package.json +1 -1
  95. package/src/background-jobs/status-reporter.js +15 -2
  96. package/src/background-jobs/worker.js +5 -1
  97. package/src/frontend-models/base.js +105 -47
  98. package/src/frontend-models/transport-deadline.js +66 -0
  99. package/src/testing/factory/association-declaration.js +50 -0
  100. package/src/testing/factory/declarations.js +110 -0
  101. package/src/testing/factory/definition-builder.js +476 -0
  102. package/src/testing/factory/errors.js +79 -0
  103. package/src/testing/factory/evaluation-context.js +199 -0
  104. package/src/testing/factory/events.js +30 -0
  105. package/src/testing/factory/factory-definition.js +41 -0
  106. package/src/testing/factory/factory-registry.js +501 -0
  107. package/src/testing/factory/factory-runner.js +296 -0
  108. package/src/testing/factory/index.js +24 -0
  109. package/src/testing/factory/linter.js +119 -0
  110. package/src/testing/factory/model-contract.js +31 -0
  111. package/src/testing/factory/node/load-definitions.js +103 -0
  112. package/src/testing/factory/sequence.js +91 -0
  113. package/src/testing/factory/strategies/attributes-for.js +24 -0
  114. package/src/testing/factory/strategies/base.js +209 -0
  115. package/src/testing/factory/strategies/build.js +42 -0
  116. package/src/testing/factory/strategies/create.js +68 -0
  117. package/src/testing/factory/trait-definition.js +24 -0
@@ -0,0 +1,296 @@
1
+ // @ts-check
2
+
3
+ import DatabaseRecord from "../../database/record/index.js"
4
+ import {FactoryCycleError, UndefinedFactoryError, UndefinedTraitError} from "./errors.js"
5
+
6
+ /**
7
+ * A resolved attribute/transient/association slot in a compiled plan.
8
+ * @typedef {object} Slot
9
+ * @property {"attribute" | "transient" | "association"} slotKind - Slot nature.
10
+ * @property {?} value - Literal/lazy value, override value, or AssociationDeclaration.
11
+ * @property {boolean} isOverride - Whether the value came from a call-site override.
12
+ */
13
+
14
+ /**
15
+ * The immutable result of compiling a factory invocation.
16
+ * @typedef {object} CompiledPlan
17
+ * @property {string} factoryName - Target factory name.
18
+ * @property {import("./factory-definition.js").default} factoryDefinition - Target definition.
19
+ * @property {(new (attributes?: Record<string, ?>) => ?) | null} modelClass - Resolved model class.
20
+ * @property {string[]} chainNames - Inheritance chain names (child last) for sequence scope.
21
+ * @property {Map<string, Slot>} resolved - Name→slot map (last declaration wins).
22
+ * @property {Map<string, import("./declarations.js").CallbackDeclaration[]>} callbacks - Deduped callbacks by event.
23
+ * @property {import("./declarations.js").InitializeWithDeclaration["fn"] | null} initializeWith - Custom constructor, or null.
24
+ * @property {import("./declarations.js").ToCreateDeclaration["fn"] | null} toCreate - Custom persistence, or null.
25
+ * @property {boolean} skipCreate - Whether persistence is skipped.
26
+ */
27
+
28
+ /**
29
+ * Compiles factory invocations into immutable plans by resolving the inheritance
30
+ * chain, expanding base and requested traits, and folding declarations into a
31
+ * name→slot map plus a deduped, ordered callback set.
32
+ */
33
+ export default class FactoryRunner {
34
+ /**
35
+ * Builds a runner.
36
+ * @param {import("./factory-registry.js").default} registry - Owning registry.
37
+ */
38
+ constructor(registry) {
39
+ /** @type {import("./factory-registry.js").default} - Owning registry. */
40
+ this.registry = registry
41
+ }
42
+
43
+ /**
44
+ * Compiles a factory invocation into a plan.
45
+ * @param {string} factoryName - Factory to run.
46
+ * @param {string[]} requestedTraits - Traits requested at the call site, in order.
47
+ * @param {Record<string, ?>} overrides - Call-site overrides (highest precedence).
48
+ * @returns {CompiledPlan} - The compiled plan.
49
+ */
50
+ compile(factoryName, requestedTraits, overrides) {
51
+ const chain = this._resolveChain(factoryName)
52
+ const target = chain[chain.length - 1]
53
+ const modelClass = this._resolveModelClass(chain)
54
+
55
+ /** @type {Array<{decl: import("./declarations.js").Declaration}>} */
56
+ const flattened = []
57
+
58
+ for (const declaration of this.registry._globalDeclarations) {
59
+ flattened.push({decl: declaration})
60
+ }
61
+
62
+ for (const factoryDefinition of chain) {
63
+ this._expandFactoryDeclarations(factoryDefinition, factoryDefinition, flattened)
64
+ }
65
+
66
+ for (const traitName of requestedTraits) {
67
+ this._expandTrait(traitName, target, flattened, [])
68
+ }
69
+
70
+ return this._buildPlan({flattened, overrides, modelClass, target, chainNames: chain.map((definition) => definition.name)})
71
+ }
72
+
73
+ /**
74
+ * Resolves the inheritance chain from the root parent down to the target.
75
+ * @param {string} factoryName - Target factory name.
76
+ * @returns {import("./factory-definition.js").default[]} - Chain (root first, target last).
77
+ */
78
+ _resolveChain(factoryName) {
79
+ /** @type {import("./factory-definition.js").default[]} */
80
+ const chain = []
81
+ /** @type {Set<string>} */
82
+ const seen = new Set()
83
+ /** @type {import("./factory-definition.js").default | null} */
84
+ let current = this._resolveFactory(factoryName)
85
+
86
+ while (current) {
87
+ if (seen.has(current.name)) {
88
+ throw new FactoryCycleError(`Factory inheritance cycle detected: ${[...seen, current.name].join(" -> ")}`)
89
+ }
90
+
91
+ seen.add(current.name)
92
+ chain.unshift(current)
93
+ current = current.parentName ? this._resolveFactory(current.parentName) : null
94
+ }
95
+
96
+ return chain
97
+ }
98
+
99
+ /**
100
+ * Resolves a factory definition by name (or alias).
101
+ * @param {string} factoryName - Factory name.
102
+ * @returns {import("./factory-definition.js").default} - The definition.
103
+ */
104
+ _resolveFactory(factoryName) {
105
+ const definition = this.registry._factories.get(factoryName)
106
+
107
+ if (!definition) {
108
+ throw new UndefinedFactoryError(`No factory registered called "${factoryName}". Registered: ${[...this.registry._factories.keys()].join(", ") || "(none)"}`)
109
+ }
110
+
111
+ return definition
112
+ }
113
+
114
+ /**
115
+ * Picks the nearest declared model class in the chain (child overrides parent).
116
+ * @param {import("./factory-definition.js").default[]} chain - Inheritance chain.
117
+ * @returns {(new (attributes?: Record<string, ?>) => ?) | null} - The model class, or null.
118
+ */
119
+ _resolveModelClass(chain) {
120
+ for (let index = chain.length - 1; index >= 0; index--) {
121
+ if (chain[index].modelClass) return chain[index].modelClass
122
+ }
123
+
124
+ return null
125
+ }
126
+
127
+ /**
128
+ * Expands one factory's own declarations, inlining base-trait inclusions.
129
+ * @param {import("./factory-definition.js").default} factoryDefinition - Factory whose declarations are expanded.
130
+ * @param {import("./factory-definition.js").default} scope - Target factory (for local-trait resolution).
131
+ * @param {Array<{decl: import("./declarations.js").Declaration}>} out - Flattened output sink.
132
+ * @returns {void}
133
+ */
134
+ _expandFactoryDeclarations(factoryDefinition, scope, out) {
135
+ for (const declaration of factoryDefinition.declarations) {
136
+ if (declaration.kind === "traitInclude") {
137
+ this._expandTrait(declaration.name, scope, out, [])
138
+ } else {
139
+ out.push({decl: declaration})
140
+ }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Expands a trait (resolving factory-local before global) and its inclusions.
146
+ * @param {string} traitName - Trait to expand.
147
+ * @param {import("./factory-definition.js").default} scope - Target factory (for local-trait resolution).
148
+ * @param {Array<{decl: import("./declarations.js").Declaration}>} out - Flattened output sink.
149
+ * @param {string[]} activePath - Trait inclusion path (for cycle detection).
150
+ * @returns {void}
151
+ */
152
+ _expandTrait(traitName, scope, out, activePath) {
153
+ if (activePath.includes(traitName)) {
154
+ throw new FactoryCycleError(`Trait inclusion cycle detected: ${[...activePath, traitName].join(" -> ")}`)
155
+ }
156
+
157
+ const localTrait = this._resolveLocalTrait(traitName, scope)
158
+ const trait = localTrait?.trait || this.registry._globalTraits.get(traitName)
159
+ const inclusionScope = localTrait?.scope || scope
160
+
161
+ if (!trait) {
162
+ throw new UndefinedTraitError(`No trait registered called "${traitName}" for factory "${scope.name}"`)
163
+ }
164
+
165
+ for (const declaration of trait.declarations) {
166
+ if (declaration.kind === "traitInclude") {
167
+ this._expandTrait(declaration.name, inclusionScope, out, [...activePath, traitName])
168
+ } else {
169
+ out.push({decl: declaration})
170
+ }
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Resolves a local trait from the current factory upward through its parents.
176
+ * @param {string} traitName - Trait name to resolve.
177
+ * @param {import("./factory-definition.js").default} scope - Declaring/requesting factory scope.
178
+ * @returns {{trait: import("./trait-definition.js").default, scope: import("./factory-definition.js").default} | undefined} - Nearest local trait and its declaring scope, if any.
179
+ */
180
+ _resolveLocalTrait(traitName, scope) {
181
+ /** @type {import("./factory-definition.js").default | undefined} */
182
+ let current = scope
183
+
184
+ while (current) {
185
+ const trait = current.localTraits.get(traitName)
186
+
187
+ if (trait) return {trait, scope: current}
188
+
189
+ current = current.parentName ? this._resolveFactory(current.parentName) : undefined
190
+ }
191
+
192
+ return undefined
193
+ }
194
+
195
+ /**
196
+ * Folds flattened declarations plus overrides into a compiled plan.
197
+ * @param {object} args - Options.
198
+ * @param {Array<{decl: import("./declarations.js").Declaration}>} args.flattened - Flattened declarations.
199
+ * @param {Record<string, ?>} args.overrides - Call-site overrides.
200
+ * @param {(new (attributes?: Record<string, ?>) => ?) | null} args.modelClass - Resolved model class.
201
+ * @param {import("./factory-definition.js").default} args.target - Target factory definition.
202
+ * @param {string[]} args.chainNames - Inheritance chain names.
203
+ * @returns {CompiledPlan} - The compiled plan.
204
+ */
205
+ _buildPlan({flattened, overrides, modelClass, target, chainNames}) {
206
+ /** @type {Map<string, Slot>} */
207
+ const resolved = new Map()
208
+ /** @type {Map<string, import("./declarations.js").CallbackDeclaration[]>} */
209
+ const callbacks = new Map()
210
+ /** @type {Set<import("./declarations.js").CallbackDeclaration>} */
211
+ const seenCallbacks = new Set()
212
+ /** @type {import("./declarations.js").InitializeWithDeclaration["fn"] | null} */
213
+ let initializeWith = null
214
+ /** @type {import("./declarations.js").ToCreateDeclaration["fn"] | null} */
215
+ let toCreate = null
216
+ let skipCreate = false
217
+
218
+ for (const {decl} of flattened) {
219
+ if (decl.kind === "attribute") {
220
+ resolved.set(decl.name, {slotKind: decl.isTransient ? "transient" : "attribute", value: decl.value, isOverride: false})
221
+ } else if (decl.kind === "association") {
222
+ resolved.set(decl.name, {slotKind: "association", value: decl, isOverride: false})
223
+ } else if (decl.kind === "callback") {
224
+ if (!seenCallbacks.has(decl)) {
225
+ seenCallbacks.add(decl)
226
+
227
+ const eventCallbacks = callbacks.get(decl.event) || []
228
+
229
+ eventCallbacks.push(decl)
230
+ callbacks.set(decl.event, eventCallbacks)
231
+ }
232
+ } else if (decl.kind === "initializeWith") {
233
+ initializeWith = decl.fn
234
+ } else if (decl.kind === "toCreate") {
235
+ toCreate = decl.fn
236
+ } else if (decl.kind === "skipCreate") {
237
+ skipCreate = true
238
+ }
239
+ }
240
+
241
+ for (const key of Object.keys(overrides)) {
242
+ const prior = resolved.get(key)
243
+
244
+ resolved.set(key, {slotKind: prior ? prior.slotKind : "attribute", value: overrides[key], isOverride: true})
245
+ }
246
+
247
+ this._arbitrateAssociationOverrides(resolved, overrides, modelClass)
248
+
249
+ return {
250
+ factoryName: target.name,
251
+ factoryDefinition: target,
252
+ modelClass,
253
+ chainNames,
254
+ resolved,
255
+ callbacks,
256
+ initializeWith,
257
+ toCreate,
258
+ skipCreate
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Applies belongs-to override precedence using the declared model relationship's
264
+ * real foreign-key metadata. An explicit association object wins over its key;
265
+ * otherwise an explicit key suppresses the factory-declared association.
266
+ * @param {Map<string, Slot>} resolved - Folded slots.
267
+ * @param {Record<string, ?>} overrides - Call-site overrides.
268
+ * @param {(new (attributes?: Record<string, ?>) => ?) | null} modelClass - Resolved model class.
269
+ * @returns {void}
270
+ */
271
+ _arbitrateAssociationOverrides(resolved, overrides, modelClass) {
272
+ if (typeof modelClass !== "function" || !(modelClass.prototype instanceof DatabaseRecord)) return
273
+
274
+ const backendModelClass = /** @type {typeof DatabaseRecord} */ (modelClass)
275
+ const columnToAttribute = backendModelClass.getColumnNameToAttributeNameMap()
276
+
277
+ for (const [name, slot] of [...resolved]) {
278
+ if (slot.slotKind !== "association") continue
279
+
280
+ const relationship = backendModelClass.getRelationshipByName(name)
281
+
282
+ if (relationship.getType() !== "belongsTo") continue
283
+
284
+ const foreignKeyColumn = relationship.getForeignKey()
285
+ const foreignKeyAttribute = columnToAttribute[foreignKeyColumn] || foreignKeyColumn
286
+
287
+ if (!Object.hasOwn(overrides, foreignKeyAttribute)) continue
288
+
289
+ if (slot.isOverride) {
290
+ resolved.delete(foreignKeyAttribute)
291
+ } else {
292
+ resolved.delete(name)
293
+ }
294
+ }
295
+ }
296
+ }
@@ -0,0 +1,24 @@
1
+ // @ts-check
2
+
3
+ import FactoryRegistry from "./factory-registry.js"
4
+
5
+ /**
6
+ * The default, convenient factory registry singleton. Import it as `Factory` and
7
+ * call `Factory.define(...)`, `Factory.build(...)`, `Factory.create(...)`,
8
+ * `Factory.attributesFor(...)` and the list/pair helpers. This module is
9
+ * browser/Metro-safe: it contains no Node built-ins, filesystem discovery, or
10
+ * raw `import.meta`. Node-only definition loading lives in `./node/load-definitions.js`.
11
+ * @type {FactoryRegistry}
12
+ */
13
+ const Factory = new FactoryRegistry()
14
+
15
+ /**
16
+ * Creates a fresh, isolated factory registry independent of the default singleton
17
+ * (for libraries or spec groups that must not share global factory state).
18
+ * @returns {FactoryRegistry} - A new registry.
19
+ */
20
+ export function createFactoryRegistry() {
21
+ return new FactoryRegistry()
22
+ }
23
+
24
+ export default Factory
@@ -0,0 +1,119 @@
1
+ // @ts-check
2
+
3
+ import {FactoryError} from "./errors.js"
4
+
5
+ /** Internal sentinel thrown to force a per-case transaction rollback. */
6
+ class LintRollbackSignal extends Error {}
7
+
8
+ /**
9
+ * Aggregated lint failure raised when one or more linted factories/traits error.
10
+ */
11
+ class FactoryLintError extends FactoryError {}
12
+
13
+ /**
14
+ * Executes registered factories (and optionally their traits) to prove they build
15
+ * and persist, aggregating every failure by case. For the create strategy each
16
+ * case runs inside the model's ambient transaction and is rolled back, so no rows
17
+ * remain in the supported single-connection case. External callback side effects
18
+ * are not reversible and cross-database writes are not globally atomic.
19
+ */
20
+ export default class FactoryLinter {
21
+ /**
22
+ * Builds a linter.
23
+ * @param {import("./factory-registry.js").default} registry - Registry to lint.
24
+ */
25
+ constructor(registry) {
26
+ /** @type {import("./factory-registry.js").default} - Registry to lint. */
27
+ this.registry = registry
28
+ }
29
+
30
+ /**
31
+ * Lints selected factories and reports every failure together.
32
+ * @param {object} [options] - Options.
33
+ * @param {string[]} [options.factories] - Factory names to lint. Defaults to all.
34
+ * @param {boolean} [options.traits] - Whether to also lint each factory's local traits.
35
+ * @param {"attributesFor" | "build" | "create"} [options.strategy] - Strategy to lint with. Defaults to create.
36
+ * @returns {Promise<void>} - Resolves when every case passed; rejects with an aggregate otherwise.
37
+ */
38
+ async lint({factories, traits = false, strategy = "create"} = {}) {
39
+ const definitions = this._selectDefinitions(factories)
40
+ /** @type {Array<{label: string, error: ?}>} */
41
+ const failures = []
42
+
43
+ for (const definition of definitions) {
44
+ await this._lintCase(definition, [], strategy, failures)
45
+
46
+ if (traits) {
47
+ for (const traitName of definition.localTraits.keys()) {
48
+ await this._lintCase(definition, [traitName], strategy, failures)
49
+ }
50
+ }
51
+ }
52
+
53
+ if (failures.length > 0) {
54
+ const details = failures.map((failure) => ` ${failure.label}: ${failure.error && failure.error.message}`).join("\n")
55
+
56
+ throw new FactoryLintError(`Factory lint found ${failures.length} error(s):\n${details}`)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Resolves the unique set of factory definitions to lint.
62
+ * @param {string[] | undefined} factories - Explicit names, or undefined for all.
63
+ * @returns {import("./factory-definition.js").default[]} - Unique definitions.
64
+ */
65
+ _selectDefinitions(factories) {
66
+ if (factories) {
67
+ return factories.map((name) => this.registry._runner._resolveFactory(name))
68
+ }
69
+
70
+ return [...new Set(this.registry._factories.values())]
71
+ }
72
+
73
+ /**
74
+ * Lints one factory/trait case, rolling back create-strategy persistence.
75
+ * @param {import("./factory-definition.js").default} definition - Factory definition.
76
+ * @param {string[]} traits - Traits to apply for this case.
77
+ * @param {"attributesFor" | "build" | "create"} strategy - Strategy to run.
78
+ * @param {Array<{label: string, error: ?}>} failures - Failure sink.
79
+ * @returns {Promise<void>} - Resolves when the case has been evaluated.
80
+ */
81
+ async _lintCase(definition, traits, strategy, failures) {
82
+ const label = traits.length > 0 ? `${definition.name} + ${traits.join(", ")}` : definition.name
83
+
84
+ try {
85
+ if (strategy === "create") {
86
+ await this._lintCreateCase(definition, traits)
87
+ } else {
88
+ await this.registry[strategy](definition.name, ...traits)
89
+ }
90
+ } catch (error) {
91
+ if (error instanceof LintRollbackSignal) return
92
+
93
+ failures.push({label, error})
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Runs a create-strategy case inside a transaction and forces a rollback.
99
+ * @param {import("./factory-definition.js").default} definition - Factory definition.
100
+ * @param {string[]} traits - Traits to apply.
101
+ * @returns {Promise<void>} - Resolves (or rejects) once the rollback completes.
102
+ */
103
+ async _lintCreateCase(definition, traits) {
104
+ const chain = this.registry._runner._resolveChain(definition.name)
105
+ const modelClass = this.registry._runner._resolveModelClass(chain)
106
+
107
+ if (!modelClass) {
108
+ await this.registry.create(definition.name, ...traits)
109
+
110
+ return
111
+ }
112
+
113
+ await /** @type {typeof import("../../database/record/index.js").default} */ (modelClass).transaction(async () => {
114
+ await this.registry.create(definition.name, ...traits)
115
+
116
+ throw new LintRollbackSignal()
117
+ })
118
+ }
119
+ }
@@ -0,0 +1,31 @@
1
+ // @ts-check
2
+
3
+ import DatabaseRecord from "../../database/record/index.js"
4
+ import {ModelContractError} from "./errors.js"
5
+
6
+ /**
7
+ * Asserts a class satisfies the V1 factory model contract: it must be an
8
+ * initialized backend Velocious `DatabaseRecord` subclass. Uninitialized backend
9
+ * classes and non-backend classes (e.g. generated frontend models) are rejected
10
+ * with a named, actionable error rather than failing deep inside construction.
11
+ * @param {?} modelClass - The candidate model class.
12
+ * @param {string} factoryName - Factory name, for the error message.
13
+ * @returns {new (attributes?: Record<string, ?>) => import("../../database/record/index.js").default} - The validated model class.
14
+ */
15
+ export function assertModelClass(modelClass, factoryName) {
16
+ if (typeof modelClass !== "function") {
17
+ throw new ModelContractError(`Factory "${factoryName}" has no model class to construct. Declare one with factory("${factoryName}", ModelClass, ...).`)
18
+ }
19
+
20
+ if (!(modelClass.prototype instanceof DatabaseRecord)) {
21
+ throw new ModelContractError(`Factory "${factoryName}" model ${modelClass.name || "class"} is not a supported Velocious backend record. build/create require an initialized DatabaseRecord subclass; generated frontend models are not supported in V1.`)
22
+ }
23
+
24
+ const backendModelClass = /** @type {typeof import("../../database/record/index.js").default} */ (modelClass)
25
+
26
+ if (!backendModelClass.isInitialized()) {
27
+ throw new ModelContractError(`Factory "${factoryName}" model ${backendModelClass.name} has not been initialized. Ensure the model class is initialized (e.g. via configuration.initialize()/ensureInitialized) before build/create.`)
28
+ }
29
+
30
+ return /** @type {new (attributes?: Record<string, ?>) => import("../../database/record/index.js").default} */ (modelClass)
31
+ }
@@ -0,0 +1,103 @@
1
+ // @ts-check
2
+
3
+ import {pathToFileURL} from "node:url"
4
+ import {readdir, stat} from "node:fs/promises"
5
+ import path from "node:path"
6
+
7
+ /**
8
+ * Monotonic cache-busting counter shared by reloads. Kept module-local so a reload
9
+ * imports a fresh module instance rather than the cached one.
10
+ * @type {number}
11
+ */
12
+ let reloadCounter = 0
13
+
14
+ /**
15
+ * Recursively collects `.js` files under a directory in a deterministic
16
+ * (lexicographically sorted) order.
17
+ * @param {string} directory - Directory to scan.
18
+ * @returns {Promise<string[]>} - Sorted absolute file paths.
19
+ */
20
+ async function collectDirectoryFiles(directory) {
21
+ const entries = await readdir(directory, {withFileTypes: true})
22
+ /** @type {string[]} */
23
+ const files = []
24
+
25
+ for (const entry of [...entries].sort((left, right) => (left.name < right.name ? -1 : 1))) {
26
+ const fullPath = path.join(directory, entry.name)
27
+
28
+ if (entry.isDirectory()) {
29
+ files.push(...await collectDirectoryFiles(fullPath))
30
+ } else if (entry.name.endsWith(".js")) {
31
+ files.push(fullPath)
32
+ }
33
+ }
34
+
35
+ return files
36
+ }
37
+
38
+ /**
39
+ * Resolves a load target (a single file, a directory, or an explicit list) into a
40
+ * deterministic, de-duplicated list of absolute definition file paths.
41
+ * @param {string | string[]} target - File path, directory, or list of paths.
42
+ * @returns {Promise<string[]>} - Sorted, de-duplicated absolute file paths.
43
+ */
44
+ async function resolveFiles(target) {
45
+ if (Array.isArray(target)) {
46
+ return [...new Set(target.map((entry) => path.resolve(entry)))].sort()
47
+ }
48
+
49
+ const resolved = path.resolve(target)
50
+ const stats = await stat(resolved)
51
+
52
+ if (stats.isDirectory()) return await collectDirectoryFiles(resolved)
53
+
54
+ return [resolved]
55
+ }
56
+
57
+ /**
58
+ * Loads Velocious factory definition files (Node only). Each file must
59
+ * default-export a `(registry) => void` function that defines into the registry.
60
+ * Files load in deterministic path order. This module is intentionally Node-only
61
+ * (filesystem + dynamic import) and must never be imported from browser/Metro
62
+ * bundles; import the browser-safe core from `../index.js` there instead.
63
+ * @param {import("../factory-registry.js").default} registry - Registry to define into.
64
+ * @param {string | string[]} target - File path, directory, or list of paths.
65
+ * @param {{reload?: boolean}} [options] - Options.
66
+ * @returns {Promise<string[]>} - The loaded file paths, in load order.
67
+ */
68
+ export async function loadDefinitions(registry, target, {reload = false} = {}) {
69
+ const files = await resolveFiles(target)
70
+
71
+ for (const file of files) {
72
+ let href = pathToFileURL(file).href
73
+
74
+ if (reload) {
75
+ reloadCounter += 1
76
+ href += `?factoryReload=${reloadCounter}`
77
+ }
78
+
79
+ const module = await import(href)
80
+
81
+ if (typeof module.default !== "function") {
82
+ throw new Error(`Factory definition file ${file} must default-export a (registry) => void function`)
83
+ }
84
+
85
+ module.default(registry)
86
+ }
87
+
88
+ return files
89
+ }
90
+
91
+ /**
92
+ * Fully reloads definitions: resets the registry (dropping every factory, trait,
93
+ * sequence, callback and default) and re-imports the target files with cache
94
+ * busting so edited definitions take effect.
95
+ * @param {import("../factory-registry.js").default} registry - Registry to reload.
96
+ * @param {string | string[]} target - File path, directory, or list of paths.
97
+ * @returns {Promise<string[]>} - The reloaded file paths, in load order.
98
+ */
99
+ export async function reloadDefinitions(registry, target) {
100
+ registry.reset()
101
+
102
+ return await loadDefinitions(registry, target, {reload: true})
103
+ }
@@ -0,0 +1,91 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * SequenceFormatter type. Receives the synchronously-allocated numeric value and
5
+ * returns the formatted value (optionally asynchronously).
6
+ * @typedef {(args: {value: number}) => (? | Promise<?>)} SequenceFormatter
7
+ */
8
+
9
+ /**
10
+ * A numeric counter sequence. Values are allocated and consumed synchronously
11
+ * before the (possibly async) formatter runs, so a rejected formatter still
12
+ * advances the counter and concurrent `Promise.all` allocation never collides —
13
+ * matching FactoryBot's failed-value-consumption behaviour.
14
+ */
15
+ export default class Sequence {
16
+ /**
17
+ * Builds a sequence.
18
+ * @param {object} args - Options.
19
+ * @param {string} args.name - Primary sequence name.
20
+ * @param {number} [args.initial] - First value the sequence yields. Defaults to 1.
21
+ * @param {SequenceFormatter} [args.formatter] - Optional formatter for the value.
22
+ * @param {string[]} [args.aliases] - Additional names that share this sequence's state.
23
+ */
24
+ constructor({name, initial = 1, formatter, aliases = []}) {
25
+ /** @type {string} - Primary sequence name. */
26
+ this.name = name
27
+
28
+ /** @type {number} - First value the sequence yields. */
29
+ this.initial = initial
30
+
31
+ /** @type {SequenceFormatter | undefined} - Optional value formatter. */
32
+ this.formatter = formatter
33
+
34
+ /** @type {string[]} - Names that share this sequence's counter. */
35
+ this.aliases = aliases
36
+
37
+ /** @type {number} - Next value to allocate. */
38
+ this._next = initial
39
+ }
40
+
41
+ /**
42
+ * Allocates and consumes the next numeric value synchronously.
43
+ * @returns {number} - The allocated raw value.
44
+ */
45
+ _allocate() {
46
+ const value = this._next
47
+
48
+ this._next += 1
49
+
50
+ return value
51
+ }
52
+
53
+ /**
54
+ * Advances the sequence and returns the formatted value. The numeric value is
55
+ * consumed synchronously before awaiting the formatter, so a rejected formatter
56
+ * still advances the counter.
57
+ * @returns {Promise<?>} - The formatted value.
58
+ */
59
+ async next() {
60
+ const value = this._allocate()
61
+
62
+ if (!this.formatter) return value
63
+
64
+ return await this.formatter({value})
65
+ }
66
+
67
+ /**
68
+ * Returns the value the next `next()` call will allocate without consuming it.
69
+ * @returns {number} - The upcoming raw value.
70
+ */
71
+ peek() {
72
+ return this._next
73
+ }
74
+
75
+ /**
76
+ * Sets the next value the sequence will allocate.
77
+ * @param {number} value - Next raw value.
78
+ * @returns {void}
79
+ */
80
+ set(value) {
81
+ this._next = value
82
+ }
83
+
84
+ /**
85
+ * Resets the counter back to its initial value.
86
+ * @returns {void}
87
+ */
88
+ rewind() {
89
+ this._next = this.initial
90
+ }
91
+ }
@@ -0,0 +1,24 @@
1
+ // @ts-check
2
+
3
+ import BaseStrategy from "./base.js"
4
+
5
+ /**
6
+ * The `attributesFor` strategy. It resolves scalar/lazy attributes (and any
7
+ * transients they depend on) but never initializes the model, runs lifecycle
8
+ * callbacks, or evaluates/builds declared associations. Transients and
9
+ * associations are omitted from the returned plain object.
10
+ */
11
+ export default class AttributesForStrategy extends BaseStrategy {
12
+ /**
13
+ * Runs the strategy.
14
+ * @param {object} args - Options.
15
+ * @param {import("../factory-registry.js").default} args.registry - Owning registry.
16
+ * @param {import("../factory-runner.js").CompiledPlan} args.plan - Compiled plan.
17
+ * @returns {Promise<Record<string, ?>>} - The resolved attributes.
18
+ */
19
+ async run({registry, plan}) {
20
+ const context = this._newContext(registry, plan, "attributesFor")
21
+
22
+ return await context.resolveAttributes()
23
+ }
24
+ }