@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.
- 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 -25
- 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
|
@@ -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
|
-
}
|
package/src/boot/traits.ts
DELETED
|
@@ -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
|
-
}
|