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.
- package/README.md +2 -0
- package/build/background-jobs/status-reporter.js +15 -2
- package/build/background-jobs/worker.js +5 -1
- package/build/frontend-models/base.js +105 -47
- package/build/frontend-models/transport-deadline.js +66 -0
- package/build/src/background-jobs/status-reporter.d.ts +3 -1
- package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
- package/build/src/background-jobs/status-reporter.js +16 -3
- package/build/src/background-jobs/worker.d.ts.map +1 -1
- package/build/src/background-jobs/worker.js +6 -2
- package/build/src/frontend-models/base.d.ts +10 -1
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +71 -26
- package/build/src/frontend-models/transport-deadline.d.ts +26 -0
- package/build/src/frontend-models/transport-deadline.d.ts.map +1 -0
- package/build/src/frontend-models/transport-deadline.js +61 -0
- package/build/src/testing/factory/association-declaration.d.ts +37 -0
- package/build/src/testing/factory/association-declaration.d.ts.map +1 -0
- package/build/src/testing/factory/association-declaration.js +41 -0
- package/build/src/testing/factory/declarations.d.ts +190 -0
- package/build/src/testing/factory/declarations.d.ts.map +1 -0
- package/build/src/testing/factory/declarations.js +98 -0
- package/build/src/testing/factory/definition-builder.d.ts +132 -0
- package/build/src/testing/factory/definition-builder.d.ts.map +1 -0
- package/build/src/testing/factory/definition-builder.js +417 -0
- package/build/src/testing/factory/errors.d.ts +62 -0
- package/build/src/testing/factory/errors.d.ts.map +1 -0
- package/build/src/testing/factory/errors.js +67 -0
- package/build/src/testing/factory/evaluation-context.d.ts +95 -0
- package/build/src/testing/factory/evaluation-context.d.ts.map +1 -0
- package/build/src/testing/factory/evaluation-context.js +175 -0
- package/build/src/testing/factory/events.d.ts +21 -0
- package/build/src/testing/factory/events.d.ts.map +1 -0
- package/build/src/testing/factory/events.js +26 -0
- package/build/src/testing/factory/factory-definition.d.ts +39 -0
- package/build/src/testing/factory/factory-definition.d.ts.map +1 -0
- package/build/src/testing/factory/factory-definition.js +35 -0
- package/build/src/testing/factory/factory-registry.d.ts +263 -0
- package/build/src/testing/factory/factory-registry.d.ts.map +1 -0
- package/build/src/testing/factory/factory-registry.js +438 -0
- package/build/src/testing/factory/factory-runner.d.ts +179 -0
- package/build/src/testing/factory/factory-runner.d.ts.map +1 -0
- package/build/src/testing/factory/factory-runner.js +266 -0
- package/build/src/testing/factory/index.d.ts +18 -0
- package/build/src/testing/factory/index.d.ts.map +1 -0
- package/build/src/testing/factory/index.js +21 -0
- package/build/src/testing/factory/linter.d.ts +55 -0
- package/build/src/testing/factory/linter.d.ts.map +1 -0
- package/build/src/testing/factory/linter.js +106 -0
- package/build/src/testing/factory/model-contract.d.ts +11 -0
- package/build/src/testing/factory/model-contract.d.ts.map +1 -0
- package/build/src/testing/factory/model-contract.js +26 -0
- package/build/src/testing/factory/node/load-definitions.d.ts +24 -0
- package/build/src/testing/factory/node/load-definitions.d.ts.map +1 -0
- package/build/src/testing/factory/node/load-definitions.js +87 -0
- package/build/src/testing/factory/sequence.d.ts +73 -0
- package/build/src/testing/factory/sequence.d.ts.map +1 -0
- package/build/src/testing/factory/sequence.js +78 -0
- package/build/src/testing/factory/strategies/attributes-for.d.ts +21 -0
- package/build/src/testing/factory/strategies/attributes-for.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/attributes-for.js +22 -0
- package/build/src/testing/factory/strategies/base.d.ts +101 -0
- package/build/src/testing/factory/strategies/base.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/base.js +191 -0
- package/build/src/testing/factory/strategies/build.d.ts +20 -0
- package/build/src/testing/factory/strategies/build.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/build.js +34 -0
- package/build/src/testing/factory/strategies/create.d.ts +32 -0
- package/build/src/testing/factory/strategies/create.d.ts.map +1 -0
- package/build/src/testing/factory/strategies/create.js +57 -0
- package/build/src/testing/factory/trait-definition.d.ts +22 -0
- package/build/src/testing/factory/trait-definition.d.ts.map +1 -0
- package/build/src/testing/factory/trait-definition.js +22 -0
- package/build/testing/factory/association-declaration.js +50 -0
- package/build/testing/factory/declarations.js +110 -0
- package/build/testing/factory/definition-builder.js +476 -0
- package/build/testing/factory/errors.js +79 -0
- package/build/testing/factory/evaluation-context.js +199 -0
- package/build/testing/factory/events.js +30 -0
- package/build/testing/factory/factory-definition.js +41 -0
- package/build/testing/factory/factory-registry.js +501 -0
- package/build/testing/factory/factory-runner.js +296 -0
- package/build/testing/factory/index.js +24 -0
- package/build/testing/factory/linter.js +119 -0
- package/build/testing/factory/model-contract.js +31 -0
- package/build/testing/factory/node/load-definitions.js +103 -0
- package/build/testing/factory/sequence.js +91 -0
- package/build/testing/factory/strategies/attributes-for.js +24 -0
- package/build/testing/factory/strategies/base.js +209 -0
- package/build/testing/factory/strategies/build.js +42 -0
- package/build/testing/factory/strategies/create.js +68 -0
- package/build/testing/factory/trait-definition.js +24 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/background-jobs/status-reporter.js +15 -2
- package/src/background-jobs/worker.js +5 -1
- package/src/frontend-models/base.js +105 -47
- package/src/frontend-models/transport-deadline.js +66 -0
- package/src/testing/factory/association-declaration.js +50 -0
- package/src/testing/factory/declarations.js +110 -0
- package/src/testing/factory/definition-builder.js +476 -0
- package/src/testing/factory/errors.js +79 -0
- package/src/testing/factory/evaluation-context.js +199 -0
- package/src/testing/factory/events.js +30 -0
- package/src/testing/factory/factory-definition.js +41 -0
- package/src/testing/factory/factory-registry.js +501 -0
- package/src/testing/factory/factory-runner.js +296 -0
- package/src/testing/factory/index.js +24 -0
- package/src/testing/factory/linter.js +119 -0
- package/src/testing/factory/model-contract.js +31 -0
- package/src/testing/factory/node/load-definitions.js +103 -0
- package/src/testing/factory/sequence.js +91 -0
- package/src/testing/factory/strategies/attributes-for.js +24 -0
- package/src/testing/factory/strategies/base.js +209 -0
- package/src/testing/factory/strategies/build.js +42 -0
- package/src/testing/factory/strategies/create.js +68 -0
- 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
|
+
}
|