@rangka/core 0.1.1 → 0.1.3

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 -25
  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,397 +0,0 @@
1
- import * as fs from 'node:fs/promises';
2
- import * as path from 'node:path';
3
- import type {
4
- ModuleConfig,
5
- ModelConfig,
6
- HooksConfig,
7
- ExtensionConfig,
8
- JobConfig,
9
- PageDefinition,
10
- RolesConfig,
11
- RangkaConfig,
12
- } from '@rangka/shared';
13
- import type { DiscoveredApp, RangkaPackageInfo } from './types.js';
14
- import type { ServiceDefinition } from '../services/types.js';
15
- import type { FixtureDefinition } from '../fixtures/types.js';
16
- import { validatePageSources, detectDuplicatePageKeys } from './page-utils.js';
17
-
18
- // ---------- Public types ----------
19
-
20
- export interface ProjectScanResult {
21
- app: DiscoveredApp;
22
- rangkaConfig: RangkaConfig;
23
- }
24
-
25
- export interface DatabaseConfig {
26
- host: string;
27
- port: number;
28
- database: string;
29
- user: string;
30
- password: string;
31
- }
32
-
33
- // ---------- Scanner ----------
34
-
35
- export class ProjectScanner {
36
- constructor(private readonly root: string) {}
37
-
38
- /**
39
- * Scans the project root and returns a fully-assembled DiscoveredApp
40
- * along with the rangka.config.ts settings.
41
- */
42
- async scan(): Promise<ProjectScanResult> {
43
- const rangkaConfig = await this.loadRangkaConfig();
44
- const modules = await this.scanModules();
45
-
46
- // Collect artifacts from every module
47
- const schemas: DiscoveredApp['schemas'] = [];
48
- const hooks: NonNullable<DiscoveredApp['hooks']> = [];
49
- const roles: NonNullable<DiscoveredApp['roles']> = [];
50
- const services: ServiceDefinition[] = [];
51
- const jobs: Array<{ name: string; config: JobConfig }> = [];
52
- const fixtures: FixtureDefinition[] = [];
53
- const pages: Array<{ module: string; page: PageDefinition }> = [];
54
-
55
- for (const moduleConfig of modules) {
56
- await this.collectModuleArtifacts(moduleConfig, {
57
- schemas,
58
- hooks,
59
- roles,
60
- services,
61
- jobs,
62
- fixtures,
63
- pages,
64
- });
65
- }
66
-
67
- const extensions = await this.scanExtensions();
68
-
69
- this.warnAboutPageIssues(pages, schemas);
70
-
71
- const app = this.buildDiscoveredApp(modules, {
72
- schemas,
73
- extensions,
74
- modules,
75
- hooks,
76
- roles,
77
- jobs,
78
- services,
79
- fixtures,
80
- pages,
81
- });
82
-
83
- return { app, rangkaConfig };
84
- }
85
-
86
- // ---------- Top-level loading ----------
87
-
88
- /** Loads rangka.config.ts from the project root. */
89
- private async loadRangkaConfig(): Promise<RangkaConfig> {
90
- const configPath = path.join(this.root, 'rangka.config.ts');
91
- await this.assertFileExists(configPath, 'No rangka.config.ts found');
92
- const mod = await this.importFile(configPath);
93
- return mod.default;
94
- }
95
-
96
- /** Discovers all sub-modules under the modules/ directory. */
97
- private async scanModules(): Promise<ModuleConfig[]> {
98
- const modulesDir = path.join(this.root, 'modules');
99
- if (!(await this.dirExists(modulesDir))) return [];
100
-
101
- const entries = await fs.readdir(modulesDir, { withFileTypes: true });
102
- const modules: ModuleConfig[] = [];
103
-
104
- for (const entry of entries) {
105
- if (!entry.isDirectory()) continue;
106
-
107
- const moduleFile = path.join(modulesDir, entry.name, 'module.ts');
108
- if (!(await this.fileExists(moduleFile))) continue;
109
-
110
- const mod = await this.importFile(moduleFile);
111
- modules.push(mod.default);
112
- }
113
-
114
- return modules;
115
- }
116
-
117
- // ---------- Per-module artifact collection ----------
118
-
119
- /**
120
- * Scans a single module's subdirectories and pushes discovered
121
- * artifacts into the corresponding accumulator arrays.
122
- */
123
- private async collectModuleArtifacts(
124
- moduleConfig: ModuleConfig,
125
- accumulators: {
126
- schemas: DiscoveredApp['schemas'];
127
- hooks: NonNullable<DiscoveredApp['hooks']>;
128
- roles: NonNullable<DiscoveredApp['roles']>;
129
- services: ServiceDefinition[];
130
- jobs: Array<{ name: string; config: JobConfig }>;
131
- fixtures: FixtureDefinition[];
132
- pages: Array<{ module: string; page: PageDefinition }>;
133
- },
134
- ): Promise<void> {
135
- const moduleName = moduleConfig.name;
136
-
137
- // Models — flat .ts files in models/
138
- const models = await this.scanModels(moduleName);
139
- for (const schema of models) {
140
- accumulators.schemas.push({ module: moduleName, schema });
141
- }
142
-
143
- // Hooks — separate hooks/ directory
144
- accumulators.hooks.push(...(await this.scanHooksDirectory(moduleName)));
145
-
146
- // Roles — per-module roles.ts file
147
- const roles = await this.scanRoles(moduleName);
148
- if (roles) {
149
- accumulators.roles.push({ config: roles, app: moduleName });
150
- }
151
-
152
- // Services, jobs, fixtures, pages
153
- accumulators.services.push(...(await this.scanServices(moduleName)));
154
- accumulators.jobs.push(...(await this.scanJobs(moduleName)));
155
- accumulators.fixtures.push(...(await this.scanFixtures(moduleName)));
156
- accumulators.pages.push(...(await this.scanPages(moduleName)));
157
- }
158
-
159
- // ---------- Model scanning (flat files) ----------
160
-
161
- /** Scans .ts files in modules/<name>/models/ for model definitions. */
162
- private async scanModels(moduleName: string): Promise<ModelConfig[]> {
163
- const modelsDir = path.join(this.root, 'modules', moduleName, 'models');
164
- return this.scanTsFilesWithDefault<ModelConfig>(modelsDir);
165
- }
166
-
167
- // ---------- Hooks scanning (separate directory) ----------
168
-
169
- /**
170
- * Scans .ts files in modules/<name>/hooks/ for hook definitions.
171
- * Each file exports defineHooks(model, config) which returns { model, ...config }.
172
- */
173
- private async scanHooksDirectory(
174
- moduleName: string,
175
- ): Promise<Array<{ model: string; hooks: HooksConfig }>> {
176
- const hooksDir = path.join(this.root, 'modules', moduleName, 'hooks');
177
- if (!(await this.dirExists(hooksDir))) return [];
178
-
179
- const entries = await fs.readdir(hooksDir, { withFileTypes: true });
180
- const result: Array<{ model: string; hooks: HooksConfig }> = [];
181
-
182
- for (const entry of entries) {
183
- if (!entry.isFile() || !entry.name.endsWith('.ts')) continue;
184
- try {
185
- const mod = await this.importFile(path.join(hooksDir, entry.name));
186
- if (mod.default) {
187
- const { model, ...hooksConfig } = mod.default;
188
- const qualifiedModel = model.includes('.') ? model : `${moduleName}.${model}`;
189
- result.push({ model: qualifiedModel, hooks: hooksConfig });
190
- }
191
- } catch (err) {
192
- console.warn(
193
- `[rangka] Failed to import hook file modules/${moduleName}/hooks/${entry.name}: ${(err as Error).message}`,
194
- );
195
- }
196
- }
197
-
198
- return result;
199
- }
200
-
201
- // ---------- Roles scanning (per-module file) ----------
202
-
203
- /** Loads modules/<name>/roles.ts if it exists. Returns the RolesConfig or null. */
204
- private async scanRoles(moduleName: string): Promise<RolesConfig | null> {
205
- const rolesFile = path.join(this.root, 'modules', moduleName, 'roles.ts');
206
- if (!(await this.fileExists(rolesFile))) return null;
207
-
208
- const mod = await this.importFile(rolesFile);
209
- return mod.default ?? null;
210
- }
211
-
212
- // ---------- Other artifact scanners ----------
213
-
214
- /** Scans .ts files in modules/<name>/services/ for service definitions. */
215
- private async scanServices(moduleName: string): Promise<ServiceDefinition[]> {
216
- const servicesDir = path.join(this.root, 'modules', moduleName, 'services');
217
- return this.scanTsFilesWithDefault<ServiceDefinition>(servicesDir);
218
- }
219
-
220
- /** Scans .ts files in modules/<name>/jobs/ for job definitions. */
221
- private async scanJobs(moduleName: string): Promise<Array<{ name: string; config: JobConfig }>> {
222
- const jobsDir = path.join(this.root, 'modules', moduleName, 'jobs');
223
- const rawJobs = await this.scanTsFilesWithDefault<{ name: string } & JobConfig>(jobsDir);
224
- return rawJobs.map(({ name, ...config }) => ({ name, config }));
225
- }
226
-
227
- /** Scans .ts files in modules/<name>/fixtures/ for fixture definitions. */
228
- private async scanFixtures(moduleName: string): Promise<FixtureDefinition[]> {
229
- const fixturesDir = path.join(this.root, 'modules', moduleName, 'fixtures');
230
- return this.scanTsFilesWithDefault<FixtureDefinition>(fixturesDir);
231
- }
232
-
233
- /** Scans .ts files in modules/<name>/pages/ for page definitions (with error handling). */
234
- private async scanPages(
235
- moduleName: string,
236
- ): Promise<Array<{ module: string; page: PageDefinition }>> {
237
- const pagesDir = path.join(this.root, 'modules', moduleName, 'pages');
238
- if (!(await this.dirExists(pagesDir))) return [];
239
-
240
- const entries = await fs.readdir(pagesDir, { withFileTypes: true });
241
- const pages: Array<{ module: string; page: PageDefinition }> = [];
242
-
243
- for (const entry of entries) {
244
- if (!entry.isFile() || !entry.name.endsWith('.ts')) continue;
245
- try {
246
- const mod = await this.importFile(path.join(pagesDir, entry.name));
247
- if (mod.default) {
248
- pages.push({ module: moduleName, page: mod.default });
249
- }
250
- } catch (err) {
251
- console.warn(
252
- `[rangka] Failed to import page file modules/${moduleName}/pages/${entry.name}: ${(err as Error).message}`,
253
- );
254
- }
255
- }
256
-
257
- return pages;
258
- }
259
-
260
- /** Scans extensions/ directory for extension definitions. */
261
- private async scanExtensions(): Promise<DiscoveredApp['extensions']> {
262
- const extensionsDir = path.join(this.root, 'extensions');
263
- if (!(await this.dirExists(extensionsDir))) return [];
264
-
265
- const entries = await fs.readdir(extensionsDir, { withFileTypes: true });
266
- const extensions: DiscoveredApp['extensions'] = [];
267
-
268
- for (const entry of entries) {
269
- if (!entry.isFile() || !entry.name.endsWith('.ts')) continue;
270
- const mod = await this.importFile(path.join(extensionsDir, entry.name));
271
- if (mod.default) {
272
- const { target, ...config } = mod.default;
273
- extensions.push({ target, config: config as ExtensionConfig });
274
- }
275
- }
276
-
277
- return extensions;
278
- }
279
-
280
- // ---------- Validation ----------
281
-
282
- /** Emits console warnings for invalid page sources or duplicate page keys. */
283
- private warnAboutPageIssues(
284
- pages: Array<{ module: string; page: PageDefinition }>,
285
- schemas: DiscoveredApp['schemas'],
286
- ): void {
287
- if (pages.length === 0) return;
288
-
289
- const knownModels = new Set(schemas.map((s) => `${s.module}.${s.schema.name}`));
290
- const sourceWarnings = validatePageSources(pages, knownModels);
291
- const duplicateWarnings = detectDuplicatePageKeys(pages);
292
-
293
- for (const warning of [...sourceWarnings, ...duplicateWarnings]) {
294
- console.warn(`[rangka] ${warning.pageKey} (${warning.location}): ${warning.message}`);
295
- }
296
- }
297
-
298
- // ---------- Assembly ----------
299
-
300
- /** Builds the final DiscoveredApp object, omitting empty optional arrays. */
301
- private buildDiscoveredApp(
302
- modules: ModuleConfig[],
303
- parts: {
304
- schemas: DiscoveredApp['schemas'];
305
- extensions: DiscoveredApp['extensions'];
306
- modules: ModuleConfig[];
307
- hooks: NonNullable<DiscoveredApp['hooks']>;
308
- roles: NonNullable<DiscoveredApp['roles']>;
309
- jobs: Array<{ name: string; config: JobConfig }>;
310
- services: ServiceDefinition[];
311
- fixtures: FixtureDefinition[];
312
- pages: Array<{ module: string; page: PageDefinition }>;
313
- },
314
- ): DiscoveredApp {
315
- const appName = modules[0]?.name ?? 'app';
316
-
317
- const packageInfo: RangkaPackageInfo = {
318
- packageName: appName,
319
- path: this.root,
320
- rangka: { type: 'app', entrypoint: './rangka.config.ts' },
321
- };
322
-
323
- const config: ModuleConfig = {
324
- name: appName,
325
- label: appName,
326
- };
327
-
328
- return {
329
- packageInfo,
330
- config,
331
- schemas: parts.schemas,
332
- extensions: parts.extensions,
333
- modules: parts.modules.length > 0 ? parts.modules : undefined,
334
- hooks: parts.hooks.length > 0 ? parts.hooks : undefined,
335
- roles: parts.roles.length > 0 ? parts.roles : undefined,
336
- jobs: parts.jobs.length > 0 ? parts.jobs : undefined,
337
- services: parts.services.length > 0 ? parts.services : undefined,
338
- fixtures: parts.fixtures.length > 0 ? parts.fixtures : undefined,
339
- pages: parts.pages.length > 0 ? parts.pages : undefined,
340
- };
341
- }
342
-
343
- // ---------- Filesystem utilities ----------
344
-
345
- /** Dynamically imports a TypeScript file by path. */
346
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
347
- private async importFile(filePath: string): Promise<any> {
348
- return import(`file://${filePath}?t=${Date.now()}`);
349
- }
350
-
351
- /** Returns true if the path exists and is a regular file. */
352
- private async fileExists(filePath: string): Promise<boolean> {
353
- try {
354
- const stat = await fs.stat(filePath);
355
- return stat.isFile();
356
- } catch {
357
- return false;
358
- }
359
- }
360
-
361
- /** Returns true if the path exists and is a directory. */
362
- private async dirExists(dirPath: string): Promise<boolean> {
363
- try {
364
- const stat = await fs.stat(dirPath);
365
- return stat.isDirectory();
366
- } catch {
367
- return false;
368
- }
369
- }
370
-
371
- /** Throws if the file does not exist. */
372
- private async assertFileExists(filePath: string, message: string): Promise<void> {
373
- if (!(await this.fileExists(filePath))) {
374
- throw new Error(`${message}: ${filePath}`);
375
- }
376
- }
377
-
378
- /**
379
- * Generic helper: reads all .ts files in a directory and collects
380
- * their default exports into an array. Skips files without a default export.
381
- * Returns an empty array if the directory doesn't exist.
382
- */
383
- private async scanTsFilesWithDefault<T>(dirPath: string): Promise<T[]> {
384
- if (!(await this.dirExists(dirPath))) return [];
385
-
386
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
387
- const results: T[] = [];
388
-
389
- for (const entry of entries) {
390
- if (!entry.isFile() || !entry.name.endsWith('.ts')) continue;
391
- const mod = await this.importFile(path.join(dirPath, entry.name));
392
- if (mod.default) results.push(mod.default);
393
- }
394
-
395
- return results;
396
- }
397
- }
@@ -1,26 +0,0 @@
1
- import type { ModelConfig, ExtensionConfig } from '@rangka/shared';
2
- import type { DiscoveredApp } from './types.js';
3
-
4
- export interface SchemaLoadResult {
5
- schemas: Array<{ app: string; module: string; schema: ModelConfig }>;
6
- extensions: Array<{ app: string; target: string; config: ExtensionConfig }>;
7
- }
8
-
9
- export function loadSchemas(apps: DiscoveredApp[]): SchemaLoadResult {
10
- const schemas: SchemaLoadResult['schemas'] = [];
11
- const extensions: SchemaLoadResult['extensions'] = [];
12
-
13
- for (const app of apps) {
14
- const appName = app.config.name;
15
-
16
- for (const { module, schema } of app.schemas) {
17
- schemas.push({ app: appName, module, schema });
18
- }
19
-
20
- for (const { target, config } of app.extensions ?? []) {
21
- extensions.push({ app: appName, target, config });
22
- }
23
- }
24
-
25
- return { schemas, extensions };
26
- }
@@ -1,125 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { ResolvedField, ResolvedModel } from '../schema/types.js';
3
- import type { SchemaLoadResult } from './schema-loader.js';
4
- import { getTraitFields } from './traits.js';
5
- import { SchemaConflictError } from './types.js';
6
-
7
- export interface MergeResult {
8
- models: ResolvedModel[];
9
- }
10
-
11
- /**
12
- * Merge loaded schemas and extensions into a flat list of resolved models.
13
- * Traits inject fields first, then base fields, then extensions add theirs.
14
- * Throws SchemaConflictError if an extension tries to redefine an existing field.
15
- */
16
- export function mergeSchemas(loadResult: SchemaLoadResult): MergeResult {
17
- const modelMap = new Map<string, ResolvedModel>();
18
-
19
- // Phase 1: Build base models from schemas (trait fields + declared fields)
20
- for (const { app, module, schema } of loadResult.schemas) {
21
- const qualifiedName = `${module}.${schema.name}`;
22
- const fields = buildBaseFields(app, schema);
23
-
24
- modelMap.set(qualifiedName, {
25
- qualifiedName,
26
- app,
27
- module,
28
- name: schema.name,
29
- label: schema.label,
30
- naming: schema.naming,
31
- scope: schema.scope,
32
- auditLog: schema.auditLog !== false,
33
- traits: schema.traits ?? [],
34
- fields,
35
- indexes: schema.indexes,
36
- });
37
- }
38
-
39
- // Phase 2: Apply extensions (add fields from other apps/modules)
40
- for (const { app, target, config } of loadResult.extensions) {
41
- const model = modelMap.get(target);
42
- if (!model) continue;
43
-
44
- if (config.fields) {
45
- applyExtensionFields(model, config.fields, app, target);
46
- }
47
- }
48
-
49
- return {
50
- models: Array.from(modelMap.values()),
51
- };
52
- }
53
-
54
- // --- Helpers ---
55
-
56
- /** Build the initial field list for a model: trait-injected fields followed by declared fields. */
57
- function buildBaseFields(
58
- app: string,
59
- schema: { name: string; traits?: string[]; fields: Record<string, any> },
60
- ): ResolvedField[] {
61
- const fields: ResolvedField[] = [];
62
-
63
- // Inject fields from each trait
64
- const traits = schema.traits ?? [];
65
- for (const trait of traits) {
66
- const traitFields = getTraitFields(trait);
67
- for (const [fieldName, fieldConfig] of Object.entries(traitFields)) {
68
- fields.push({
69
- name: fieldName,
70
- config: fieldConfig,
71
- provenance: { source: 'trait', trait },
72
- });
73
- }
74
- }
75
-
76
- // Add the model's own declared fields
77
- for (const [fieldName, fieldConfig] of Object.entries(schema.fields)) {
78
- fields.push({
79
- name: fieldName,
80
- config: fieldConfig,
81
- provenance: { source: 'base', app },
82
- });
83
- }
84
-
85
- return fields;
86
- }
87
-
88
- /** Append extension fields to a model, throwing on conflicts with existing fields. */
89
- function applyExtensionFields(
90
- model: ResolvedModel,
91
- extensionFields: Record<string, any>,
92
- extensionApp: string,
93
- target: string,
94
- ): void {
95
- for (const [fieldName, fieldConfig] of Object.entries(extensionFields)) {
96
- const existing = model.fields.find((f) => f.name === fieldName);
97
- if (existing) {
98
- const existingSource = describeProvenance(existing);
99
- throw new SchemaConflictError(
100
- target,
101
- fieldName,
102
- existingSource,
103
- `extension (${extensionApp})`,
104
- );
105
- }
106
-
107
- model.fields.push({
108
- name: fieldName,
109
- config: fieldConfig,
110
- provenance: { source: 'extension', app: extensionApp },
111
- });
112
- }
113
- }
114
-
115
- /** Produce a human-readable description of where a field came from. */
116
- function describeProvenance(field: ResolvedField): string {
117
- switch (field.provenance.source) {
118
- case 'base':
119
- return `base schema (${field.provenance.app})`;
120
- case 'extension':
121
- return `extension (${field.provenance.app})`;
122
- case 'trait':
123
- return `trait (${field.provenance.trait})`;
124
- }
125
- }
@@ -1,25 +0,0 @@
1
- import type { FieldConfig } from '@rangka/shared';
2
-
3
- export interface TraitFields {
4
- [fieldName: string]: FieldConfig;
5
- }
6
-
7
- const timestampedFields: TraitFields = {
8
- created_at: { type: 'datetime' },
9
- updated_at: { type: 'datetime' },
10
- created_by: { type: 'link', model: 'core.user' },
11
- updated_by: { type: 'link', model: 'core.user' },
12
- };
13
-
14
- const softDeleteFields: TraitFields = {
15
- archived_at: { type: 'datetime' },
16
- };
17
-
18
- const traitFieldMap: Record<string, TraitFields> = {
19
- timestamped: timestampedFields,
20
- soft_delete: softDeleteFields,
21
- };
22
-
23
- export function getTraitFields(trait: string): TraitFields {
24
- return traitFieldMap[trait] ?? {};
25
- }
package/src/boot/types.ts DELETED
@@ -1,73 +0,0 @@
1
- import type {
2
- ModuleConfig,
3
- ModelConfig,
4
- ExtensionConfig,
5
- HooksConfig,
6
- RolesConfig,
7
- JobConfig,
8
- PageDefinition,
9
- WidgetDefinitionMeta,
10
- } from '@rangka/shared';
11
- import type { ApiDefinition } from '../api/types.js';
12
- import type { ServiceDefinition } from '../services/types.js';
13
- import type { FixtureDefinition } from '../fixtures/types.js';
14
-
15
- export interface RangkaPackageInfo {
16
- packageName: string;
17
- path: string;
18
- rangka: {
19
- type: 'app';
20
- entrypoint: string;
21
- };
22
- }
23
-
24
- export interface DiscoveredApp {
25
- packageInfo: RangkaPackageInfo;
26
- config: ModuleConfig;
27
- schemas: Array<{ module: string; schema: ModelConfig }>;
28
- extensions: Array<{ target: string; config: ExtensionConfig }>;
29
- modules?: ModuleConfig[];
30
- hooks?: Array<{ model: string; hooks: HooksConfig }>;
31
- roles?: Array<{ config: RolesConfig; app: string }>;
32
- jobs?: Array<{ name: string; config: JobConfig }>;
33
- services?: ServiceDefinition[];
34
- apiDefinitions?: ApiDefinition[];
35
- fixtures?: FixtureDefinition[];
36
- pages?: Array<{ module: string; page: PageDefinition }>;
37
- widgets?: WidgetDefinitionMeta[];
38
- }
39
-
40
- export interface DiscoverySource {
41
- findRangkaPackages(): Promise<RangkaPackageInfo[]>;
42
- }
43
-
44
- export class SchemaConflictError extends Error {
45
- constructor(
46
- public readonly model: string,
47
- public readonly field: string,
48
- public readonly sourceA: string,
49
- public readonly sourceB: string,
50
- ) {
51
- super(
52
- `Schema conflict on model "${model}", field "${field}": declared by both "${sourceA}" and "${sourceB}"`,
53
- );
54
- this.name = 'SchemaConflictError';
55
- }
56
- }
57
-
58
- export class MissingDependencyError extends Error {
59
- constructor(
60
- public readonly app: string,
61
- public readonly missingDep: string,
62
- ) {
63
- super(`App "${app}" depends on "${missingDep}", which was not found in discovered packages`);
64
- this.name = 'MissingDependencyError';
65
- }
66
- }
67
-
68
- export class CircularDependencyError extends Error {
69
- constructor(public readonly cycle: string[]) {
70
- super(`Circular dependency detected: ${cycle.join(' → ')}`);
71
- this.name = 'CircularDependencyError';
72
- }
73
- }