@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,107 +0,0 @@
1
- import type { TranslatedFilter } from '../model-api/filter-translator.js';
2
- import { toBool, isNil } from '../helpers/coerce.js';
3
-
4
- export function applyInMemoryFilters(
5
- records: Record<string, unknown>[],
6
- filters: TranslatedFilter[],
7
- ): Record<string, unknown>[] {
8
- return records.filter((record) => {
9
- for (const { field, operator, value } of filters) {
10
- const fieldValue = record[field];
11
-
12
- switch (operator) {
13
- case 'eq':
14
- if (fieldValue !== value) return false;
15
- break;
16
- case 'neq':
17
- if (fieldValue === value) return false;
18
- break;
19
- case 'gt':
20
- if (!(fieldValue! > value!)) return false;
21
- break;
22
- case 'gte':
23
- if (!(fieldValue! >= value!)) return false;
24
- break;
25
- case 'lt':
26
- if (!(fieldValue! < value!)) return false;
27
- break;
28
- case 'lte':
29
- if (!(fieldValue! <= value!)) return false;
30
- break;
31
- case 'in':
32
- if (!(value as unknown[]).includes(fieldValue)) return false;
33
- break;
34
- case 'notIn':
35
- if ((value as unknown[]).includes(fieldValue)) return false;
36
- break;
37
- case 'contains':
38
- if (
39
- typeof fieldValue !== 'string' ||
40
- !fieldValue.toLowerCase().includes((value as string).toLowerCase())
41
- )
42
- return false;
43
- break;
44
- case 'startsWith':
45
- if (
46
- typeof fieldValue !== 'string' ||
47
- !fieldValue.toLowerCase().startsWith((value as string).toLowerCase())
48
- )
49
- return false;
50
- break;
51
- case 'endsWith':
52
- if (
53
- typeof fieldValue !== 'string' ||
54
- !fieldValue.toLowerCase().endsWith((value as string).toLowerCase())
55
- )
56
- return false;
57
- break;
58
- case 'is':
59
- case 'isnull':
60
- if (isNil(value) || toBool(value)) {
61
- if (!isNil(fieldValue)) return false;
62
- } else {
63
- if (isNil(fieldValue)) return false;
64
- }
65
- break;
66
- case 'like':
67
- if (
68
- typeof fieldValue !== 'string' ||
69
- !fieldValue.toLowerCase().includes((value as string).toLowerCase())
70
- )
71
- return false;
72
- break;
73
- }
74
- }
75
- return true;
76
- });
77
- }
78
-
79
- export function applyInMemorySort(
80
- records: Record<string, unknown>[],
81
- sorts: Array<{ field: string; direction: 'asc' | 'desc' }>,
82
- ): Record<string, unknown>[] {
83
- if (sorts.length === 0) return records;
84
-
85
- return [...records].sort((a, b) => {
86
- for (const { field, direction } of sorts) {
87
- const aVal = a[field];
88
- const bVal = b[field];
89
-
90
- if (aVal === bVal) continue;
91
- if (aVal == null) return direction === 'asc' ? -1 : 1;
92
- if (bVal == null) return direction === 'asc' ? 1 : -1;
93
-
94
- const cmp = aVal < bVal ? -1 : 1;
95
- return direction === 'asc' ? cmp : -cmp;
96
- }
97
- return 0;
98
- });
99
- }
100
-
101
- export function applyInMemoryPagination(
102
- records: Record<string, unknown>[],
103
- limit: number,
104
- offset: number,
105
- ): Record<string, unknown>[] {
106
- return records.slice(offset, offset + limit);
107
- }
@@ -1,7 +0,0 @@
1
- export { defineExternalModel } from './define.js';
2
- export { mapAdapterResponse, reverseMapForWrite, resolveFieldValue } from './field-mapper.js';
3
- export { evaluateComputedFields } from './computed-fields.js';
4
- export { externalModelToResolved } from './schema-converter.js';
5
- export type { ExternalModelConfig, ExternalFieldConfig, ComputedFieldConfig } from './types.js';
6
- export { ExternalModelOps } from './external-model-ops.js';
7
- export type { ExternalModelOpsConfig } from './external-model-ops.js';
@@ -1,71 +0,0 @@
1
- import type { DataAdapter, AdapterCapability } from '../plugins/types.js';
2
- import type { ExternalFieldConfig } from './types.js';
3
- import { reverseMapForWrite, mapAdapterResponse } from './field-mapper.js';
4
- import { evaluateComputedFields } from './computed-fields.js';
5
-
6
- export class CapabilityNotSupportedError extends Error {
7
- constructor(
8
- public readonly adapterName: string,
9
- public readonly operation: string,
10
- ) {
11
- super(`Adapter "${adapterName}" does not support operation "${operation}"`);
12
- this.name = 'CapabilityNotSupportedError';
13
- }
14
- }
15
-
16
- export interface ExternalMutationOptions {
17
- adapter: DataAdapter;
18
- adapterName: string;
19
- modelName: string;
20
- fields: Record<string, ExternalFieldConfig>;
21
- capabilities: AdapterCapability[];
22
- }
23
-
24
- export class ExternalMutationExecutor {
25
- private readonly adapter: DataAdapter;
26
- private readonly adapterName: string;
27
- private readonly modelName: string;
28
- private readonly fields: Record<string, ExternalFieldConfig>;
29
- private readonly capabilities: Set<AdapterCapability>;
30
-
31
- constructor(options: ExternalMutationOptions) {
32
- this.adapter = options.adapter;
33
- this.adapterName = options.adapterName;
34
- this.modelName = options.modelName;
35
- this.fields = options.fields;
36
- this.capabilities = new Set(options.capabilities);
37
- }
38
-
39
- async create(data: Record<string, unknown>): Promise<Record<string, unknown>> {
40
- if (!this.capabilities.has('create') || !this.adapter.create) {
41
- throw new CapabilityNotSupportedError(this.adapterName, 'create');
42
- }
43
-
44
- const mapped = reverseMapForWrite(data, this.fields);
45
- const raw = await this.adapter.create(this.modelName, mapped);
46
- return this.transformRecord(raw);
47
- }
48
-
49
- async update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>> {
50
- if (!this.capabilities.has('update') || !this.adapter.update) {
51
- throw new CapabilityNotSupportedError(this.adapterName, 'update');
52
- }
53
-
54
- const mapped = reverseMapForWrite(data, this.fields);
55
- const raw = await this.adapter.update(this.modelName, id, mapped);
56
- return this.transformRecord(raw);
57
- }
58
-
59
- async delete(id: string): Promise<void> {
60
- if (!this.capabilities.has('delete') || !this.adapter.delete) {
61
- throw new CapabilityNotSupportedError(this.adapterName, 'delete');
62
- }
63
-
64
- await this.adapter.delete(this.modelName, id);
65
- }
66
-
67
- private transformRecord(raw: Record<string, unknown>): Record<string, unknown> {
68
- const mapped = mapAdapterResponse(raw, this.fields);
69
- return evaluateComputedFields(mapped, this.fields);
70
- }
71
- }
@@ -1,100 +0,0 @@
1
- import type { DataAdapter, ListQuery } from '../plugins/types.js';
2
- import type { ExternalFieldConfig } from './types.js';
3
- import type { TranslatedFilter } from '../model-api/filter-translator.js';
4
- import { mapAdapterResponse } from './field-mapper.js';
5
- import { evaluateComputedFields } from './computed-fields.js';
6
- import {
7
- applyInMemoryFilters,
8
- applyInMemorySort,
9
- applyInMemoryPagination,
10
- } from './in-memory-ops.js';
11
- import type { AdapterCapability } from '../plugins/types.js';
12
-
13
- export interface ExternalQueryOptions {
14
- adapter: DataAdapter;
15
- modelName: string;
16
- fields: Record<string, ExternalFieldConfig>;
17
- capabilities: AdapterCapability[];
18
- }
19
-
20
- export interface ExternalQueryState {
21
- filters: TranslatedFilter[];
22
- sorts: Array<{ field: string; direction: 'asc' | 'desc' }>;
23
- limitVal?: number;
24
- offsetVal?: number;
25
- pageVal?: number;
26
- fieldNames: string[];
27
- }
28
-
29
- export class ExternalQueryExecutor {
30
- private readonly adapter: DataAdapter;
31
- private readonly modelName: string;
32
- private readonly fields: Record<string, ExternalFieldConfig>;
33
- private readonly capabilities: Set<AdapterCapability>;
34
-
35
- constructor(options: ExternalQueryOptions) {
36
- this.adapter = options.adapter;
37
- this.modelName = options.modelName;
38
- this.fields = options.fields;
39
- this.capabilities = new Set(options.capabilities);
40
- }
41
-
42
- async execGet(id: string): Promise<Record<string, unknown> | null> {
43
- const raw = await this.adapter.get(this.modelName, id);
44
- if (!raw) return null;
45
- return this.transformRecord(raw);
46
- }
47
-
48
- async execList(
49
- state: ExternalQueryState,
50
- ): Promise<{ data: Record<string, unknown>[]; total?: number; hasMore?: boolean }> {
51
- const limit = state.limitVal ?? 25;
52
- const page = state.pageVal ?? 1;
53
-
54
- if (this.capabilities.has('list')) {
55
- const query: ListQuery = { pageSize: limit, page };
56
-
57
- if (state.filters.length > 0 && this.capabilities.has('filter')) {
58
- query.filters = state.filters.map((f) => ({
59
- field: f.field,
60
- operator: f.operator,
61
- value: f.value,
62
- }));
63
- }
64
-
65
- if (state.sorts.length > 0 && this.capabilities.has('sort')) {
66
- query.sort = state.sorts[0];
67
- }
68
-
69
- const result = await this.adapter.list!(this.modelName, query);
70
- let data = result.data.map((r) => this.transformRecord(r));
71
-
72
- if (state.filters.length > 0 && !this.capabilities.has('filter')) {
73
- data = applyInMemoryFilters(data, state.filters);
74
- }
75
-
76
- if (state.sorts.length > 0 && !this.capabilities.has('sort')) {
77
- data = applyInMemorySort(data, state.sorts);
78
- }
79
-
80
- if (!this.capabilities.has('filter') || !this.capabilities.has('sort')) {
81
- data = applyInMemoryPagination(data, limit, 0);
82
- }
83
-
84
- return { data, total: result.total, hasMore: result.hasMore };
85
- }
86
-
87
- // Fallback: no list capability, fetch all via repeated get (not practical for real use)
88
- return { data: [], total: 0 };
89
- }
90
-
91
- async execCount(state: ExternalQueryState): Promise<number> {
92
- const result = await this.execList(state);
93
- return result.total ?? result.data.length;
94
- }
95
-
96
- private transformRecord(raw: Record<string, unknown>): Record<string, unknown> {
97
- const mapped = mapAdapterResponse(raw, this.fields);
98
- return evaluateComputedFields(mapped, this.fields);
99
- }
100
- }
@@ -1,53 +0,0 @@
1
- import type { ResolvedModel, ResolvedField, ModelRelationship } from '../schema/types.js';
2
- import type { ExternalModelConfig } from './types.js';
3
-
4
- export interface ExternalModelConversionResult {
5
- model: ResolvedModel;
6
- relationships: ModelRelationship[];
7
- }
8
-
9
- export function externalModelToResolved(
10
- config: ExternalModelConfig,
11
- app: string,
12
- module: string,
13
- ): ExternalModelConversionResult {
14
- const qualifiedName = `${module}.${config.name}`;
15
-
16
- const fields: ResolvedField[] = Object.entries(config.fields).map(([name, fieldConfig]) => ({
17
- name,
18
- config: {
19
- type: fieldConfig.type,
20
- label: fieldConfig.label,
21
- required: fieldConfig.required,
22
- },
23
- provenance: { source: 'base' as const },
24
- }));
25
-
26
- const relationships: ModelRelationship[] = [];
27
- for (const [fieldName, fieldConfig] of Object.entries(config.fields)) {
28
- if (!fieldConfig.relationship) continue;
29
-
30
- relationships.push({
31
- type: fieldConfig.relationship.type,
32
- from: qualifiedName,
33
- field: fieldName,
34
- to: fieldConfig.relationship.model,
35
- foreignKey: fieldConfig.relationship.foreignKey,
36
- });
37
- }
38
-
39
- const model: ResolvedModel = {
40
- qualifiedName,
41
- app,
42
- module,
43
- name: config.name,
44
- label: config.label,
45
- auditLog: false,
46
- traits: [],
47
- fields,
48
- indexes: [],
49
- source: config.source,
50
- };
51
-
52
- return { model, relationships };
53
- }
@@ -1,32 +0,0 @@
1
- export interface ExternalFieldMapping {
2
- from?: string;
3
- }
4
-
5
- export interface ComputedFieldConfig {
6
- depends: string[];
7
- compute: (record: Record<string, unknown>) => unknown;
8
- }
9
-
10
- export interface ExternalRelationshipConfig {
11
- type: 'link' | 'hasMany';
12
- model: string;
13
- from?: string;
14
- foreignKey?: string;
15
- }
16
-
17
- export interface ExternalFieldConfig {
18
- type: 'string' | 'int' | 'decimal' | 'boolean' | 'date' | 'datetime' | 'json';
19
- label?: string;
20
- required?: boolean;
21
- from?: string;
22
- computed?: ComputedFieldConfig;
23
- relationship?: ExternalRelationshipConfig;
24
- }
25
-
26
- export interface ExternalModelConfig {
27
- name: string;
28
- source: string;
29
- module?: string;
30
- label?: string;
31
- fields: Record<string, ExternalFieldConfig>;
32
- }
@@ -1,203 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { FixtureRegistry } from '../registry.js';
3
- import { loadFixtures } from '../loader.js';
4
- import type { FixtureDefinition } from '../types.js';
5
-
6
- describe('FixtureRegistry', () => {
7
- it('registers and retrieves fixtures', () => {
8
- const registry = new FixtureRegistry();
9
- registry.register({
10
- model: 'accounting.account',
11
- key: 'account_code',
12
- records: [{ account_code: '1000', name: 'Assets' }],
13
- });
14
- expect(registry.getForModel('accounting.account')).toHaveLength(1);
15
- });
16
-
17
- it('rejects fixture with empty model', () => {
18
- const registry = new FixtureRegistry();
19
- expect(() => registry.register({ model: '', key: 'code', records: [{ code: '1' }] })).toThrow(
20
- 'model must not be empty',
21
- );
22
- });
23
-
24
- it('rejects fixture with no records', () => {
25
- const registry = new FixtureRegistry();
26
- expect(() => registry.register({ model: 'x.y', key: 'code', records: [] })).toThrow(
27
- 'at least one record',
28
- );
29
- });
30
-
31
- it('resolves dependency order', () => {
32
- const registry = new FixtureRegistry();
33
- registry.register({
34
- model: 'accounting.tax',
35
- key: 'name',
36
- depends: ['accounting.account'],
37
- records: [{ name: 'VAT' }],
38
- });
39
- registry.register({
40
- model: 'accounting.account',
41
- key: 'code',
42
- records: [{ code: '1000' }],
43
- });
44
-
45
- const order = registry.getLoadOrder();
46
- const models = order.map((f) => f.model);
47
- expect(models.indexOf('accounting.account')).toBeLessThan(models.indexOf('accounting.tax'));
48
- });
49
-
50
- it('filters by variant with fallback to default', () => {
51
- const registry = new FixtureRegistry();
52
- registry.register({
53
- model: 'accounting.account',
54
- key: 'code',
55
- records: [{ code: '1000', name: 'Default' }],
56
- });
57
- registry.register({
58
- model: 'accounting.account',
59
- key: 'code',
60
- variant: 'ID',
61
- records: [{ code: '1-1000', name: 'Indonesia' }],
62
- });
63
- registry.register({
64
- model: 'accounting.tax',
65
- key: 'name',
66
- records: [{ name: 'VAT' }],
67
- });
68
-
69
- const filtered = registry.getForVariant('ID');
70
- const models = filtered.map((f) => f.model);
71
- expect(models).toContain('accounting.account');
72
- expect(models).toContain('accounting.tax');
73
- // Should use ID variant for account
74
- const accountFixture = filtered.find((f) => f.model === 'accounting.account');
75
- expect(accountFixture?.records[0].name).toBe('Indonesia');
76
- });
77
- });
78
-
79
- describe('Fixture Loader', () => {
80
- function createMockDb() {
81
- const tables: Record<string, any[]> = {};
82
-
83
- const db: any = {
84
- selectFrom: (table: string) => {
85
- const rows = tables[table] ?? [];
86
- const filters: Array<{ col: string; val: any }> = [];
87
- const builder: any = {
88
- selectAll: () => builder,
89
- select: () => builder,
90
- where: (col: string, op: string, val: any) => {
91
- filters.push({ col, val });
92
- return builder;
93
- },
94
- execute: async () => {
95
- return rows.filter((r) => filters.every((f) => r[f.col] === f.val));
96
- },
97
- };
98
- return builder;
99
- },
100
- insertInto: (table: string) => ({
101
- values: (data: any) => ({
102
- execute: async () => {
103
- if (!tables[table]) tables[table] = [];
104
- tables[table].push({ id: `id-${tables[table].length + 1}`, ...data });
105
- },
106
- }),
107
- }),
108
- updateTable: (table: string) => ({
109
- set: (data: any) => ({
110
- where: (col: string, op: string, val: any) => ({
111
- execute: async () => {
112
- const rows = tables[table] ?? [];
113
- const row = rows.find((r) => r[col] === val);
114
- if (row) Object.assign(row, data);
115
- },
116
- }),
117
- }),
118
- }),
119
- };
120
-
121
- return { db, tables };
122
- }
123
-
124
- it('inserts new records', async () => {
125
- const { db, tables } = createMockDb();
126
- const defs: FixtureDefinition[] = [
127
- {
128
- model: 'accounting.account',
129
- key: 'account_code',
130
- records: [
131
- { account_code: '1000', name: 'Assets' },
132
- { account_code: '2000', name: 'Liabilities' },
133
- ],
134
- },
135
- ];
136
-
137
- const result = await loadFixtures(db, defs);
138
- expect(result.inserted).toBe(2);
139
- expect(result.skipped).toBe(0);
140
- expect(tables['accounting_account']).toHaveLength(2);
141
- });
142
-
143
- it('skips user-modified records (no fixture hash)', async () => {
144
- const { db, tables } = createMockDb();
145
- tables['accounting_account'] = [
146
- { account_code: '1000', name: 'User Modified', _fixture_hash: undefined },
147
- ];
148
-
149
- const defs: FixtureDefinition[] = [
150
- {
151
- model: 'accounting.account',
152
- key: 'account_code',
153
- records: [{ account_code: '1000', name: 'Assets' }],
154
- },
155
- ];
156
-
157
- const result = await loadFixtures(db, defs);
158
- expect(result.skipped).toBe(1);
159
- expect(tables['accounting_account'][0].name).toBe('User Modified');
160
- });
161
-
162
- it('force mode overwrites existing records', async () => {
163
- const { db, tables } = createMockDb();
164
- tables['accounting_account'] = [{ account_code: '1000', name: 'Old', _fixture_hash: null }];
165
-
166
- const defs: FixtureDefinition[] = [
167
- {
168
- model: 'accounting.account',
169
- key: 'account_code',
170
- records: [{ account_code: '1000', name: 'Updated' }],
171
- },
172
- ];
173
-
174
- const result = await loadFixtures(db, defs, { force: true });
175
- expect(result.inserted).toBe(1);
176
- expect(tables['accounting_account'][0].name).toBe('Updated');
177
- });
178
-
179
- it('resolves ref fields to record IDs', async () => {
180
- const { db, tables } = createMockDb();
181
- tables['accounting_account'] = [
182
- { id: 'acct-uuid', account_code: '2100', name: 'Tax Payable', _fixture_hash: 'x' },
183
- ];
184
-
185
- const defs: FixtureDefinition[] = [
186
- {
187
- model: 'accounting.tax',
188
- key: 'name',
189
- records: [
190
- {
191
- name: 'VAT 11%',
192
- rate: 11,
193
- account: { ref: 'accounting.account', key: '2100' },
194
- },
195
- ],
196
- },
197
- ];
198
-
199
- const result = await loadFixtures(db, defs);
200
- expect(result.inserted).toBe(1);
201
- expect(tables['accounting_tax'][0].account).toBe('acct-uuid');
202
- });
203
- });
@@ -1,10 +0,0 @@
1
- export { FixtureRegistry } from './registry.js';
2
- export { loadFixtures } from './loader.js';
3
- export type {
4
- FixtureDefinition,
5
- FixtureRef,
6
- FixtureRecord,
7
- FixtureStatus,
8
- FixtureLoadResult,
9
- RegisteredFixture,
10
- } from './types.js';