@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.
Files changed (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -18
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. package/vitest.config.ts +0 -14
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- });