@rangka/core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -18
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
package/src/fixtures/loader.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
import type { FixtureDefinition, FixtureLoadResult, FixtureRef } from './types.js';
|
|
3
|
-
|
|
4
|
-
// --- Hashing ---
|
|
5
|
-
|
|
6
|
-
/** Produces a short deterministic hash of a record for change detection. */
|
|
7
|
-
function computeHash(record: Record<string, unknown>): string {
|
|
8
|
-
const sortedKeys = Object.keys(record).sort();
|
|
9
|
-
const json = JSON.stringify(record, sortedKeys);
|
|
10
|
-
return createHash('sha256').update(json).digest('hex').slice(0, 16);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// --- Reference resolution ---
|
|
14
|
-
|
|
15
|
-
/** Type guard: checks whether a field value is a cross-model reference. */
|
|
16
|
-
function isRef(value: unknown): value is FixtureRef {
|
|
17
|
-
return (
|
|
18
|
-
typeof value === 'object' &&
|
|
19
|
-
value !== null &&
|
|
20
|
-
'ref' in value &&
|
|
21
|
-
'key' in value &&
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
-
typeof (value as any).ref === 'string' &&
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
-
typeof (value as any).key === 'string'
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Infers the lookup field name for a reference when none is explicitly provided.
|
|
31
|
-
* E.g. model "tenant.role" -> lookup field "role_code".
|
|
32
|
-
*/
|
|
33
|
-
function inferLookupField(model: string): string {
|
|
34
|
-
const segments = model.split('.');
|
|
35
|
-
const modelName = segments[segments.length - 1];
|
|
36
|
-
return `${modelName}_code`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parses a reference key string into its lookup field and value.
|
|
41
|
-
*
|
|
42
|
-
* Format: "field:value" uses the explicit field, otherwise the field is
|
|
43
|
-
* inferred from the referenced model name.
|
|
44
|
-
*/
|
|
45
|
-
function parseRefKey(
|
|
46
|
-
refKey: string,
|
|
47
|
-
refModel: string,
|
|
48
|
-
): { lookupField: string; lookupValue: string } {
|
|
49
|
-
if (refKey.includes(':')) {
|
|
50
|
-
const [lookupField, lookupValue] = refKey.split(':');
|
|
51
|
-
return { lookupField, lookupValue };
|
|
52
|
-
}
|
|
53
|
-
return { lookupField: inferLookupField(refModel), lookupValue: refKey };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Resolves all FixtureRef values in a record into actual foreign-key IDs
|
|
58
|
-
* by querying the database for the referenced rows.
|
|
59
|
-
*/
|
|
60
|
-
async function resolveRefs(
|
|
61
|
-
record: Record<string, unknown>,
|
|
62
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
-
db: any,
|
|
64
|
-
): Promise<Record<string, unknown>> {
|
|
65
|
-
const resolved = { ...record };
|
|
66
|
-
|
|
67
|
-
for (const [field, value] of Object.entries(resolved)) {
|
|
68
|
-
if (!isRef(value)) continue;
|
|
69
|
-
|
|
70
|
-
const tableName = value.ref.replace('.', '_');
|
|
71
|
-
const { lookupField, lookupValue } = parseRefKey(value.key, value.ref);
|
|
72
|
-
|
|
73
|
-
const rows = await db
|
|
74
|
-
.selectFrom(tableName)
|
|
75
|
-
.select('id')
|
|
76
|
-
.where(lookupField, '=', lookupValue)
|
|
77
|
-
.execute();
|
|
78
|
-
|
|
79
|
-
resolved[field] = rows.length > 0 ? rows[0].id : null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return resolved;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// --- Core loader ---
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Converts a model name (e.g. "tenant.user") into its database table name.
|
|
89
|
-
*/
|
|
90
|
-
function toTableName(model: string): string {
|
|
91
|
-
return model.replace('.', '_');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Inserts or updates a single fixture record depending on whether it already
|
|
96
|
-
* exists and whether it has changed since the last load.
|
|
97
|
-
*/
|
|
98
|
-
async function upsertFixtureRecord(
|
|
99
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
-
db: any,
|
|
101
|
-
tableName: string,
|
|
102
|
-
definition: FixtureDefinition,
|
|
103
|
-
record: Record<string, unknown>,
|
|
104
|
-
options: { force?: boolean },
|
|
105
|
-
): Promise<'inserted' | 'skipped'> {
|
|
106
|
-
const keyValue = record[definition.key] as string;
|
|
107
|
-
const fixtureHash = computeHash(record);
|
|
108
|
-
|
|
109
|
-
const existingRows = await db
|
|
110
|
-
.selectFrom(tableName)
|
|
111
|
-
.selectAll()
|
|
112
|
-
.where(definition.key, '=', keyValue)
|
|
113
|
-
.execute();
|
|
114
|
-
|
|
115
|
-
// New record: insert it.
|
|
116
|
-
if (existingRows.length === 0) {
|
|
117
|
-
const resolved = await resolveRefs(record, db);
|
|
118
|
-
await db
|
|
119
|
-
.insertInto(tableName)
|
|
120
|
-
.values({
|
|
121
|
-
...resolved,
|
|
122
|
-
_fixture_source: definition.model,
|
|
123
|
-
_fixture_hash: fixtureHash,
|
|
124
|
-
})
|
|
125
|
-
.execute();
|
|
126
|
-
return 'inserted';
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Force mode: always overwrite.
|
|
130
|
-
if (options.force) {
|
|
131
|
-
const resolved = await resolveRefs(record, db);
|
|
132
|
-
await db
|
|
133
|
-
.updateTable(tableName)
|
|
134
|
-
.set({
|
|
135
|
-
...resolved,
|
|
136
|
-
_fixture_source: definition.model,
|
|
137
|
-
_fixture_hash: fixtureHash,
|
|
138
|
-
})
|
|
139
|
-
.where(definition.key, '=', keyValue)
|
|
140
|
-
.execute();
|
|
141
|
-
return 'inserted';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Existing record: update only if the fixture hash has changed.
|
|
145
|
-
const existingHash = existingRows[0]._fixture_hash;
|
|
146
|
-
const hasChanged = existingHash && existingHash !== fixtureHash;
|
|
147
|
-
|
|
148
|
-
if (!hasChanged) {
|
|
149
|
-
return 'skipped';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const resolved = await resolveRefs(record, db);
|
|
153
|
-
await db
|
|
154
|
-
.updateTable(tableName)
|
|
155
|
-
.set({
|
|
156
|
-
...resolved,
|
|
157
|
-
_fixture_hash: fixtureHash,
|
|
158
|
-
})
|
|
159
|
-
.where(definition.key, '=', keyValue)
|
|
160
|
-
.execute();
|
|
161
|
-
return 'inserted';
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Loads an array of fixture definitions into the database.
|
|
166
|
-
*
|
|
167
|
-
* Each record is inserted if new, updated if its content hash changed,
|
|
168
|
-
* or skipped if unchanged. Pass `options.force` to always overwrite.
|
|
169
|
-
*/
|
|
170
|
-
export async function loadFixtures(
|
|
171
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
172
|
-
db: any,
|
|
173
|
-
definitions: FixtureDefinition[],
|
|
174
|
-
options?: { force?: boolean },
|
|
175
|
-
): Promise<FixtureLoadResult> {
|
|
176
|
-
let inserted = 0;
|
|
177
|
-
let skipped = 0;
|
|
178
|
-
let total = 0;
|
|
179
|
-
|
|
180
|
-
for (const definition of definitions) {
|
|
181
|
-
const tableName = toTableName(definition.model);
|
|
182
|
-
|
|
183
|
-
for (const record of definition.records) {
|
|
184
|
-
total++;
|
|
185
|
-
const result = await upsertFixtureRecord(db, tableName, definition, record, options ?? {});
|
|
186
|
-
|
|
187
|
-
if (result === 'inserted') {
|
|
188
|
-
inserted++;
|
|
189
|
-
} else {
|
|
190
|
-
skipped++;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return { inserted, skipped, total };
|
|
196
|
-
}
|
package/src/fixtures/registry.ts
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { FixtureDefinition, RegisteredFixture } from './types.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Stores fixture definitions keyed by model name.
|
|
5
|
-
* Fixtures can be retrieved in dependency-sorted order for safe loading.
|
|
6
|
-
*/
|
|
7
|
-
export class FixtureRegistry {
|
|
8
|
-
private readonly fixtures: Map<string, RegisteredFixture[]> = new Map();
|
|
9
|
-
|
|
10
|
-
/** Validate and store a fixture definition. */
|
|
11
|
-
register(definition: FixtureDefinition): void {
|
|
12
|
-
const errors = this.validate(definition);
|
|
13
|
-
if (errors.length > 0) {
|
|
14
|
-
throw new Error(`Invalid fixture for "${definition.model}": ${errors.join('; ')}`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const existing = this.fixtures.get(definition.model) ?? [];
|
|
18
|
-
existing.push({ model: definition.model, definition });
|
|
19
|
-
this.fixtures.set(definition.model, existing);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
getForModel(model: string): RegisteredFixture[] {
|
|
23
|
-
return this.fixtures.get(model) ?? [];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
getAll(): RegisteredFixture[] {
|
|
27
|
-
const all: RegisteredFixture[] = [];
|
|
28
|
-
for (const fixtures of this.fixtures.values()) {
|
|
29
|
-
all.push(...fixtures);
|
|
30
|
-
}
|
|
31
|
-
return all;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
getAllModels(): string[] {
|
|
35
|
-
return Array.from(this.fixtures.keys());
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Return all definitions sorted so dependencies load first. */
|
|
39
|
-
getLoadOrder(): FixtureDefinition[] {
|
|
40
|
-
const allDefinitions = this.getAll().map((f) => f.definition);
|
|
41
|
-
return this.topologicalSort(allDefinitions);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get fixtures for a specific variant. Variant fixtures override base (no-variant)
|
|
46
|
-
* fixtures on a per-model basis. Results are dependency-sorted.
|
|
47
|
-
*/
|
|
48
|
-
getForVariant(variant: string | undefined): FixtureDefinition[] {
|
|
49
|
-
const allDefinitions = this.getAll().map((f) => f.definition);
|
|
50
|
-
|
|
51
|
-
if (!variant) {
|
|
52
|
-
return allDefinitions.filter((f) => !f.variant);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Start with base fixtures, then let variant fixtures override per-model
|
|
56
|
-
const fixtureByModel = new Map<string, FixtureDefinition>();
|
|
57
|
-
for (const fixture of allDefinitions) {
|
|
58
|
-
if (!fixture.variant) {
|
|
59
|
-
fixtureByModel.set(fixture.model, fixture);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
for (const fixture of allDefinitions) {
|
|
63
|
-
if (fixture.variant === variant) {
|
|
64
|
-
fixtureByModel.set(fixture.model, fixture);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return this.topologicalSort(Array.from(fixtureByModel.values()));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Sort definitions so that each fixture's dependencies appear before it.
|
|
73
|
-
* Uses depth-first traversal with visited tracking.
|
|
74
|
-
*/
|
|
75
|
-
private topologicalSort(definitions: FixtureDefinition[]): FixtureDefinition[] {
|
|
76
|
-
const definitionByModel = new Map<string, FixtureDefinition>();
|
|
77
|
-
for (const def of definitions) {
|
|
78
|
-
definitionByModel.set(def.model, def);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const visited = new Set<string>();
|
|
82
|
-
const sorted: FixtureDefinition[] = [];
|
|
83
|
-
|
|
84
|
-
const visit = (model: string) => {
|
|
85
|
-
if (visited.has(model)) return;
|
|
86
|
-
visited.add(model);
|
|
87
|
-
|
|
88
|
-
const def = definitionByModel.get(model);
|
|
89
|
-
if (!def) return;
|
|
90
|
-
|
|
91
|
-
// Visit dependencies first
|
|
92
|
-
if (def.depends) {
|
|
93
|
-
for (const dependency of def.depends) {
|
|
94
|
-
visit(dependency);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
sorted.push(def);
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
for (const model of definitionByModel.keys()) {
|
|
102
|
-
visit(model);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return sorted;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private validate(definition: FixtureDefinition): string[] {
|
|
109
|
-
const errors: string[] = [];
|
|
110
|
-
|
|
111
|
-
if (!definition.model || definition.model.trim().length === 0) {
|
|
112
|
-
errors.push('Fixture model must not be empty');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!definition.key || definition.key.trim().length === 0) {
|
|
116
|
-
errors.push('Fixture key must not be empty');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (!definition.records || definition.records.length === 0) {
|
|
120
|
-
errors.push('Fixture must have at least one record');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return errors;
|
|
124
|
-
}
|
|
125
|
-
}
|
package/src/fixtures/types.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
export interface FixtureRef {
|
|
2
|
-
ref: string;
|
|
3
|
-
key: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export interface FixtureDefinition {
|
|
7
|
-
model: string;
|
|
8
|
-
key: string;
|
|
9
|
-
variant?: string;
|
|
10
|
-
depends?: string[];
|
|
11
|
-
records: Array<Record<string, unknown>>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type FixtureStatus = 'pending' | 'loaded' | 'skipped';
|
|
15
|
-
|
|
16
|
-
export interface FixtureRecord {
|
|
17
|
-
model: string;
|
|
18
|
-
key: string;
|
|
19
|
-
keyValue: string;
|
|
20
|
-
fixtureHash: string;
|
|
21
|
-
status: FixtureStatus;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface RegisteredFixture {
|
|
25
|
-
model: string;
|
|
26
|
-
definition: FixtureDefinition;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface FixtureLoadResult {
|
|
30
|
-
inserted: number;
|
|
31
|
-
skipped: number;
|
|
32
|
-
total: number;
|
|
33
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { ResolvedModel } from '../schema/types.js';
|
|
2
|
-
import type { ResolvedPermissions } from '../auth/types.js';
|
|
3
|
-
import { isOwnerOnly, modelHasCreatedBy } from '../auth/model-permissions.js';
|
|
4
|
-
import { ForbiddenError } from '../errors.js';
|
|
5
|
-
|
|
6
|
-
export function assertOwnership(
|
|
7
|
-
permissions: ResolvedPermissions | undefined,
|
|
8
|
-
model: ResolvedModel,
|
|
9
|
-
record: Record<string, unknown>,
|
|
10
|
-
userId: string | undefined,
|
|
11
|
-
operation: 'read' | 'write' | 'delete',
|
|
12
|
-
): void {
|
|
13
|
-
if (!isOwnerOnly(permissions, model.qualifiedName, operation)) return;
|
|
14
|
-
|
|
15
|
-
if (!modelHasCreatedBy(model) || record.created_by !== userId) {
|
|
16
|
-
const action = operation === 'delete' ? 'delete' : 'update';
|
|
17
|
-
throw new ForbiddenError('FORBIDDEN', `You can only ${action} records you created`);
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/helpers/coerce.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type coercion helpers for normalizing values from query params,
|
|
3
|
-
* database results, and other loosely-typed boundaries.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export function toBool(value: unknown): boolean {
|
|
7
|
-
return value === true || value === 'true' || value === 1 || value === '1';
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function toInt(value: unknown, fallback: number = 0): number {
|
|
11
|
-
if (isNil(value) || value === '') return fallback;
|
|
12
|
-
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
13
|
-
return Math.trunc(value);
|
|
14
|
-
}
|
|
15
|
-
const parsed = Number(value);
|
|
16
|
-
if (isNaN(parsed) || !Number.isFinite(parsed)) return fallback;
|
|
17
|
-
return Math.trunc(parsed);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function isNil(value: unknown): value is null | undefined {
|
|
21
|
-
return value === null || value === undefined;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function toCount(result: unknown): number {
|
|
25
|
-
if (isNil(result)) return 0;
|
|
26
|
-
const raw = typeof result === 'object' ? (result as Record<string, unknown>).count : result;
|
|
27
|
-
return toInt(raw, 0);
|
|
28
|
-
}
|
package/src/helpers/stamping.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { ResolvedModel } from '../schema/types.js';
|
|
2
|
-
|
|
3
|
-
export function stampCreate(
|
|
4
|
-
body: Record<string, unknown>,
|
|
5
|
-
model: ResolvedModel,
|
|
6
|
-
auth?: { user?: { id: string } | null },
|
|
7
|
-
): void {
|
|
8
|
-
if (!model.traits.includes('timestamped')) return;
|
|
9
|
-
const now = new Date().toISOString();
|
|
10
|
-
body.created_at = now;
|
|
11
|
-
body.updated_at = now;
|
|
12
|
-
if (auth?.user?.id) {
|
|
13
|
-
body.created_by = auth.user.id;
|
|
14
|
-
body.updated_by = auth.user.id;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function stampUpdate(
|
|
19
|
-
body: Record<string, unknown>,
|
|
20
|
-
model: ResolvedModel,
|
|
21
|
-
auth?: { user?: { id: string } | null },
|
|
22
|
-
): void {
|
|
23
|
-
if (!model.traits.includes('timestamped')) return;
|
|
24
|
-
body.updated_at = new Date().toISOString();
|
|
25
|
-
if (auth?.user?.id) {
|
|
26
|
-
body.updated_by = auth.user.id;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { ResolvedModel } from '../schema/types.js';
|
|
2
|
-
|
|
3
|
-
export function findMissingRequiredFields(
|
|
4
|
-
model: ResolvedModel,
|
|
5
|
-
body: Record<string, unknown>,
|
|
6
|
-
): string[] {
|
|
7
|
-
const missing: string[] = [];
|
|
8
|
-
for (const field of model.fields) {
|
|
9
|
-
if ('required' in field.config && field.config.required && body[field.name] === undefined) {
|
|
10
|
-
missing.push(field.name);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
return missing;
|
|
14
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createHookContext } from '../context.js';
|
|
3
|
-
import type { SchemaRegistry } from '../../schema/registry.js';
|
|
4
|
-
import type { RequestContext } from '../../auth/types.js';
|
|
5
|
-
|
|
6
|
-
function mockSchema(): SchemaRegistry {
|
|
7
|
-
return { getAllModels: () => [] } as unknown as SchemaRegistry;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function mockAuth(): RequestContext {
|
|
11
|
-
return {
|
|
12
|
-
user: { id: '1', email: 'test@test.com' },
|
|
13
|
-
roles: ['Admin'],
|
|
14
|
-
scopeFilters: [],
|
|
15
|
-
} as unknown as RequestContext;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('createHookContext', () => {
|
|
19
|
-
it('returns context with db, schema, and auth', () => {
|
|
20
|
-
const trx = { fake: 'transaction' };
|
|
21
|
-
const schema = mockSchema();
|
|
22
|
-
const auth = mockAuth();
|
|
23
|
-
|
|
24
|
-
const ctx = createHookContext({ trx, schema, auth });
|
|
25
|
-
|
|
26
|
-
expect(ctx.db).toBe(trx);
|
|
27
|
-
expect(ctx.schema).toBe(schema);
|
|
28
|
-
expect(ctx.auth.user).toEqual(auth.user);
|
|
29
|
-
expect(ctx.auth.roles).toEqual(auth.roles);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('enqueue calls the enqueue function with the trx', async () => {
|
|
33
|
-
const ctx = createHookContext({ trx: {}, schema: mockSchema(), auth: mockAuth() });
|
|
34
|
-
// Without a real db, it will throw a type error from kysely - confirms wiring is in place
|
|
35
|
-
await expect(ctx.enqueue('test-job', {})).rejects.toThrow();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('service throws when no serviceRegistry provided', () => {
|
|
39
|
-
const ctx = createHookContext({ trx: {}, schema: mockSchema(), auth: mockAuth() });
|
|
40
|
-
expect(() => ctx.service('test-service')).toThrow('ServiceRegistry not available');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('service resolves via serviceRegistry when provided', () => {
|
|
44
|
-
const mockRegistry = {
|
|
45
|
-
get: (name: string) => ({ greet: () => `hello from ${name}` }),
|
|
46
|
-
} as any;
|
|
47
|
-
const ctx = createHookContext({
|
|
48
|
-
trx: {},
|
|
49
|
-
schema: mockSchema(),
|
|
50
|
-
auth: mockAuth(),
|
|
51
|
-
serviceRegistry: mockRegistry,
|
|
52
|
-
});
|
|
53
|
-
const svc = ctx.service('greeter') as any;
|
|
54
|
-
expect(svc.greet()).toBe('hello from greeter');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('events.emit is a no-op without eventBus', async () => {
|
|
58
|
-
const ctx = createHookContext({ trx: {}, schema: mockSchema(), auth: mockAuth() });
|
|
59
|
-
await expect(ctx.events.emit('test-event', {})).resolves.toBeUndefined();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('events.emit delegates to eventBus when provided', async () => {
|
|
63
|
-
const emitWithTrx = async () => {};
|
|
64
|
-
const mockBus = { emitWithTrx } as any;
|
|
65
|
-
const ctx = createHookContext({
|
|
66
|
-
trx: {},
|
|
67
|
-
schema: mockSchema(),
|
|
68
|
-
auth: mockAuth(),
|
|
69
|
-
eventBus: mockBus,
|
|
70
|
-
});
|
|
71
|
-
await expect(ctx.events.emit('test-event', {})).resolves.toBeUndefined();
|
|
72
|
-
});
|
|
73
|
-
});
|