@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,146 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { FastifyRequest, FastifyReply } from 'fastify';
3
- import type { ResolvedModel } from '../schema/types.js';
4
- import type { DatabaseClient } from '../db/client.js';
5
- import type { ScopeFilter, RequestContext } from './types.js';
6
- import type { ScopeRegistry } from './scope-registry.js';
7
- import { getAuthContext } from './session.js';
8
- import { BadRequestError, ForbiddenError } from '../errors.js';
9
- import { isNil } from '../helpers/coerce.js';
10
-
11
- export { applyScopeFiltersToQuery } from './scope-filters.js';
12
-
13
- export interface ScopeHookContext {
14
- model: ResolvedModel;
15
- scopeRegistry: ScopeRegistry;
16
- db: DatabaseClient;
17
- filterProviders?: FilterProvider[];
18
- }
19
-
20
- export type FilterProvider = (
21
- model: ResolvedModel,
22
- authCtx: RequestContext,
23
- request: FastifyRequest,
24
- ) => ScopeFilter[] | Promise<ScopeFilter[]>;
25
-
26
- export function createScopeHook(ctx: ScopeHookContext) {
27
- const { model, scopeRegistry, db, filterProviders } = ctx;
28
- const binding = scopeRegistry.getModelBinding(model.qualifiedName);
29
-
30
- return async function scopeHook(request: FastifyRequest, _reply: FastifyReply): Promise<void> {
31
- const authCtx = getAuthContext(request);
32
- if (!authCtx.permissions || !authCtx.user) return;
33
-
34
- const filters: ScopeFilter[] = [];
35
-
36
- if (binding) {
37
- const activeValue = resolveActiveScopeValue(request, binding.scopeName, authCtx.user);
38
- if (!activeValue) {
39
- throw new BadRequestError(
40
- 'MISSING_SCOPE',
41
- `Active scope value for "${binding.scopeName}" is required. Set it via X-Active-Scope header or user default.`,
42
- );
43
- }
44
-
45
- const exists = await validateScopeValueExists(db, binding.scopeModel, activeValue);
46
- if (!exists) {
47
- throw new BadRequestError(
48
- 'INVALID_SCOPE',
49
- `Scope value "${activeValue}" does not exist in "${binding.scopeModel}".`,
50
- );
51
- }
52
-
53
- filters.push({ field: binding.column, operator: 'eq', value: activeValue });
54
- }
55
-
56
- if (filterProviders) {
57
- for (const provider of filterProviders) {
58
- const extra = await provider(model, authCtx, request);
59
- filters.push(...extra);
60
- }
61
- }
62
-
63
- authCtx.scopeFilters = filters;
64
- (request as any).authContext = authCtx;
65
- };
66
- }
67
-
68
- export function createScopeWriteGuard(ctx: ScopeHookContext) {
69
- const { model, scopeRegistry } = ctx;
70
- const binding = scopeRegistry.getModelBinding(model.qualifiedName);
71
-
72
- return async function scopeWriteGuard(
73
- request: FastifyRequest,
74
- _reply: FastifyReply,
75
- ): Promise<void> {
76
- if (request.method === 'GET') return;
77
- if (!binding) return;
78
-
79
- const authCtx = getAuthContext(request);
80
- if (!authCtx.scopeFilters?.length) return;
81
-
82
- const body = request.body as Record<string, unknown> | undefined;
83
- if (!body) return;
84
-
85
- const scopeFilter = authCtx.scopeFilters.find((f) => f.field === binding.column);
86
- if (!scopeFilter) return;
87
-
88
- const fieldValue = body[binding.column];
89
-
90
- if (request.method === 'POST') {
91
- if (isNil(fieldValue)) {
92
- body[binding.column] = scopeFilter.value;
93
- return;
94
- }
95
- }
96
-
97
- if (fieldValue !== undefined && fieldValue !== scopeFilter.value) {
98
- throw new ForbiddenError(
99
- 'SCOPE_VIOLATION',
100
- `Cannot write to scope "${binding.scopeName}": ${binding.column} must be "${scopeFilter.value}".`,
101
- );
102
- }
103
- };
104
- }
105
-
106
- function resolveActiveScopeValue(
107
- request: FastifyRequest,
108
- scopeName: string,
109
- user: Record<string, unknown>,
110
- ): string | undefined {
111
- const headerValue = parseScopeHeader(request);
112
- if (headerValue?.[scopeName]) {
113
- return String(headerValue[scopeName]);
114
- }
115
-
116
- const defaultField = `default_${scopeName}`;
117
- if (user[defaultField]) {
118
- return String(user[defaultField]);
119
- }
120
-
121
- return undefined;
122
- }
123
-
124
- function parseScopeHeader(request: FastifyRequest): Record<string, string> | undefined {
125
- const header = request.headers['x-active-scope'];
126
- if (!header || typeof header !== 'string') return undefined;
127
-
128
- try {
129
- return JSON.parse(header);
130
- } catch {
131
- return undefined;
132
- }
133
- }
134
-
135
- async function validateScopeValueExists(
136
- db: DatabaseClient,
137
- scopeModel: string,
138
- value: string,
139
- ): Promise<boolean> {
140
- const result = await db
141
- .selectFrom(scopeModel)
142
- .where('id', '=', value)
143
- .selectAll()
144
- .executeTakeFirst();
145
- return result !== undefined;
146
- }
package/src/auth/seed.ts DELETED
@@ -1,44 +0,0 @@
1
- import type { DatabaseClient } from '../db/client.js';
2
- import { hashPassword } from './password.js';
3
-
4
- export async function seedCoreData(db: DatabaseClient): Promise<void> {
5
- const existing = await db
6
- .selectFrom('core.user')
7
- .select('id')
8
- .where('email', '=', 'system@rangka.local')
9
- .executeTakeFirst();
10
-
11
- if (existing) return;
12
-
13
- const adminRole = await db
14
- .insertInto('core.role')
15
- .values({
16
- id: crypto.randomUUID(),
17
- name: 'Administrator',
18
- inherits: JSON.stringify([]),
19
- permissions: JSON.stringify({}),
20
- })
21
- .returningAll()
22
- .executeTakeFirstOrThrow();
23
-
24
- const systemUser = await db
25
- .insertInto('core.user')
26
- .values({
27
- id: crypto.randomUUID(),
28
- email: 'system@rangka.local',
29
- password_hash: hashPassword('admin'),
30
- full_name: 'System Administrator',
31
- enabled: true,
32
- })
33
- .returningAll()
34
- .executeTakeFirstOrThrow();
35
-
36
- await db
37
- .insertInto('core.user_role')
38
- .values({
39
- id: crypto.randomUUID(),
40
- user_id: systemUser.id,
41
- role_id: adminRole.id,
42
- })
43
- .execute();
44
- }
@@ -1,178 +0,0 @@
1
- import { randomBytes } from 'node:crypto';
2
- import type { FastifyRequest, FastifyReply } from 'fastify';
3
- import type { DatabaseClient } from '../db/client.js';
4
- import { BadRequestError, UnauthorizedError } from '../errors.js';
5
- import type { PermissionRegistry } from './permission-registry.js';
6
- import type { AuthUser, AuthSession, RequestContext } from './types.js';
7
-
8
- const TOKEN_BYTES = 32;
9
- const SESSION_DURATION_MS = 24 * 60 * 60 * 1000;
10
-
11
- export function generateToken(): string {
12
- return randomBytes(TOKEN_BYTES).toString('hex');
13
- }
14
-
15
- // Extracts Bearer token, validates the session, loads the user and their permissions,
16
- // then attaches everything to request.authContext for downstream guards.
17
- export function createAuthHook(db: DatabaseClient, permissionRegistry: PermissionRegistry) {
18
- return async function authHook(request: FastifyRequest, _reply: FastifyReply): Promise<void> {
19
- const ctx: RequestContext = {};
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- (request as any).authContext = ctx;
22
-
23
- const token = extractBearerToken(request);
24
- if (!token) {
25
- throw new UnauthorizedError('Missing or invalid Authorization header');
26
- }
27
-
28
- const session = await findActiveSession(db, token);
29
- if (!session) {
30
- throw new UnauthorizedError('Invalid or expired session token');
31
- }
32
-
33
- const user = await findEnabledUser(db, session.user_id);
34
- if (!user) {
35
- throw new UnauthorizedError('User not found or disabled');
36
- }
37
-
38
- const roleNames = await getUserRoleNames(db, user.id);
39
- const permissions = permissionRegistry.resolvePermissionsForRoles(roleNames);
40
-
41
- ctx.user = user;
42
- ctx.session = session;
43
- ctx.permissions = permissions;
44
- ctx.roles = roleNames;
45
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
- (request as any).authContext = ctx;
47
- };
48
- }
49
-
50
- export function getAuthContext(request: FastifyRequest): RequestContext {
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- return (request as any).authContext ?? {};
53
- }
54
-
55
- // --- Session CRUD handlers ---
56
-
57
- export interface SessionCreateBody {
58
- email: string;
59
- password: string;
60
- }
61
-
62
- // POST /session — authenticates with email/password, returns a session token.
63
- export function createSessionHandler(db: DatabaseClient) {
64
- return async (request: FastifyRequest, reply: FastifyReply) => {
65
- const { verifyPassword } = await import('./password.js');
66
- const body = request.body as SessionCreateBody | undefined;
67
-
68
- if (!body?.email || !body?.password) {
69
- throw new BadRequestError('VALIDATION_ERROR', 'Email and password are required');
70
- }
71
-
72
- const user = await findEnabledUser(db, body.email, 'email');
73
- if (!user) {
74
- throw new UnauthorizedError('Invalid credentials');
75
- }
76
-
77
- if (!verifyPassword(body.password, user.password_hash)) {
78
- throw new UnauthorizedError('Invalid credentials');
79
- }
80
-
81
- const session = await insertSession(db, user.id);
82
-
83
- return reply.status(201).send({
84
- data: { token: session.token, expires_at: session.expires_at },
85
- });
86
- };
87
- }
88
-
89
- // DELETE /session — destroys the current session.
90
- export function deleteSessionHandler(db: DatabaseClient) {
91
- return async (request: FastifyRequest, reply: FastifyReply) => {
92
- const ctx = getAuthContext(request);
93
- if (!ctx.session) {
94
- throw new UnauthorizedError('Not authenticated');
95
- }
96
-
97
- await db.deleteFrom('core.session').where('id', '=', ctx.session.id).execute();
98
- return reply.status(204).send();
99
- };
100
- }
101
-
102
- // Invalidates all existing sessions for a user and creates a fresh one.
103
- export async function regenerateSessionToken(db: DatabaseClient, userId: string): Promise<string> {
104
- await db.deleteFrom('core.session').where('user_id', '=', userId).execute();
105
- const session = await insertSession(db, userId);
106
- return session.token;
107
- }
108
-
109
- // --- Helpers ---
110
-
111
- function extractBearerToken(request: FastifyRequest): string | null {
112
- const header = request.headers.authorization;
113
- if (!header?.startsWith('Bearer ')) return null;
114
- const token = header.slice(7);
115
- return token || null;
116
- }
117
-
118
- async function findActiveSession(db: DatabaseClient, token: string): Promise<AuthSession | null> {
119
- const session = (await db
120
- .selectFrom('core.session')
121
- .selectAll()
122
- .where('token', '=', token)
123
- .executeTakeFirst()) as AuthSession | undefined;
124
-
125
- if (!session) return null;
126
-
127
- const isExpired = new Date(session.expires_at).getTime() < Date.now();
128
- return isExpired ? null : session;
129
- }
130
-
131
- async function findEnabledUser(
132
- db: DatabaseClient,
133
- value: string,
134
- by: 'id' | 'email' = 'id',
135
- ): Promise<AuthUser | null> {
136
- const user = (await db
137
- .selectFrom('core.user')
138
- .selectAll()
139
- .where(by, '=', value)
140
- .executeTakeFirst()) as AuthUser | undefined;
141
-
142
- if (!user || !user.enabled) return null;
143
- return user;
144
- }
145
-
146
- async function getUserRoleNames(db: DatabaseClient, userId: string): Promise<string[]> {
147
- const userRoles = await db
148
- .selectFrom('core.user_role')
149
- .selectAll()
150
- .where('user_id', '=', userId)
151
- .execute();
152
-
153
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
- const roleIds = userRoles.map((ur: any) => ur.role_id);
155
- if (roleIds.length === 0) return [];
156
-
157
- const roles = await db.selectFrom('core.role').selectAll().where('id', 'in', roleIds).execute();
158
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
- return roles.map((r: any) => r.name);
160
- }
161
-
162
- async function insertSession(db: DatabaseClient, userId: string) {
163
- const token = generateToken();
164
- const now = new Date();
165
- const expiresAt = new Date(now.getTime() + SESSION_DURATION_MS);
166
-
167
- return await db
168
- .insertInto('core.session')
169
- .values({
170
- id: crypto.randomUUID(),
171
- token,
172
- user_id: userId,
173
- expires_at: expiresAt.toISOString(),
174
- created_at: now.toISOString(),
175
- })
176
- .returningAll()
177
- .executeTakeFirstOrThrow();
178
- }
package/src/auth/types.ts DELETED
@@ -1,50 +0,0 @@
1
- import type { RolesConfig, ModelPermissions } from '@rangka/shared';
2
-
3
- export interface AuthUser {
4
- id: string;
5
- email: string;
6
- full_name: string;
7
- enabled: boolean;
8
- password_hash: string;
9
- [key: string]: unknown;
10
- }
11
-
12
- export interface AuthSession {
13
- id: string;
14
- token: string;
15
- user_id: string;
16
- expires_at: Date;
17
- created_at: Date;
18
- permission_version?: number;
19
- }
20
-
21
- export interface ResolvedPermissions {
22
- models: Record<string, ModelPermissions>;
23
- pages: string[];
24
- version: number;
25
- }
26
-
27
- export interface RequestContext {
28
- user?: AuthUser;
29
- session?: AuthSession;
30
- permissions?: ResolvedPermissions;
31
- roles?: string[];
32
- scopeFilters?: ScopeFilter[];
33
- }
34
-
35
- export interface ScopeFilter {
36
- field: string;
37
- operator: string;
38
- value: unknown;
39
- }
40
-
41
- export interface RegisteredRole {
42
- name: string;
43
- config: RolesConfig[string];
44
- app: string;
45
- }
46
-
47
- export interface PermissionCacheEntry {
48
- permissions: ResolvedPermissions;
49
- version: number;
50
- }
@@ -1,170 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import * as path from 'node:path';
3
- import { ProjectScanner } from '../project-scanner.js';
4
- import { validatePageSources, detectDuplicatePageKeys } from '../page-utils.js';
5
- import type { PageDefinition } from '@rangka/shared';
6
-
7
- const FIXTURE_ROOT = path.resolve(__dirname, '../../../../../tests/fixtures/basic-app');
8
-
9
- describe('ProjectScanner - page scanning', () => {
10
- it('discovers pages from modules with pages directory', async () => {
11
- const scanner = new ProjectScanner(FIXTURE_ROOT);
12
- const result = await scanner.scan();
13
-
14
- expect(result.app.pages).toBeDefined();
15
- expect(result.app.pages!.length).toBe(2);
16
-
17
- const pageKeys = result.app.pages!.map((p) => p.page.key);
18
- expect(pageKeys).toContain('sales.customers');
19
- expect(pageKeys).toContain('sales.orders');
20
-
21
- for (const entry of result.app.pages!) {
22
- expect(entry.module).toBe('sales');
23
- }
24
- });
25
-
26
- it('returns no pages field when no pages directory exists', async () => {
27
- const scanner = new ProjectScanner(
28
- path.resolve(__dirname, '../../../../../tests/fixtures/basic-app'),
29
- );
30
- const result = await scanner.scan();
31
-
32
- if (result.app.pages) {
33
- expect(result.app.pages.length).toBeGreaterThan(0);
34
- }
35
- });
36
-
37
- it('logs warning and continues when a page file has import error', async () => {
38
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
39
-
40
- const scanner = new ProjectScanner(FIXTURE_ROOT);
41
- const result = await scanner.scan();
42
-
43
- expect(result.app.pages).toBeDefined();
44
- expect(result.app.pages!.length).toBeGreaterThan(0);
45
-
46
- warnSpy.mockRestore();
47
- });
48
- });
49
-
50
- describe('detectDuplicatePageKeys', () => {
51
- it('emits warning for duplicate page keys', () => {
52
- const pages: Array<{ module: string; page: PageDefinition }> = [
53
- {
54
- module: 'sales',
55
- page: { key: 'sales.orders', label: 'Orders', type: 'collection', body: [] },
56
- },
57
- {
58
- module: 'crm',
59
- page: { key: 'sales.orders', label: 'Orders CRM', type: 'collection', body: [] },
60
- },
61
- ];
62
-
63
- const warnings = detectDuplicatePageKeys(pages);
64
- expect(warnings).toHaveLength(1);
65
- expect(warnings[0].pageKey).toBe('sales.orders');
66
- expect(warnings[0].message).toContain('Duplicate page key');
67
- });
68
-
69
- it('no warnings when all keys are unique', () => {
70
- const pages: Array<{ module: string; page: PageDefinition }> = [
71
- {
72
- module: 'sales',
73
- page: { key: 'sales.orders', label: 'Orders', type: 'collection', body: [] },
74
- },
75
- {
76
- module: 'sales',
77
- page: { key: 'sales.customers', label: 'Customers', type: 'collection', body: [] },
78
- },
79
- ];
80
-
81
- const warnings = detectDuplicatePageKeys(pages);
82
- expect(warnings).toHaveLength(0);
83
- });
84
- });
85
-
86
- describe('validatePageSources', () => {
87
- const knownModels = new Set(['sales.order', 'sales.customer', 'contacts.contact']);
88
-
89
- it('passes when all source models exist', () => {
90
- const pages: Array<{ module: string; page: PageDefinition }> = [
91
- {
92
- module: 'sales',
93
- page: {
94
- key: 'sales.orders',
95
- label: 'Orders',
96
- type: 'collection',
97
- body: [{ type: 'data', source: { model: 'sales.order' }, children: [] }],
98
- },
99
- },
100
- ];
101
-
102
- const warnings = validatePageSources(pages, knownModels);
103
- expect(warnings).toHaveLength(0);
104
- });
105
-
106
- it('warns when body widget references non-existent model', () => {
107
- const pages: Array<{ module: string; page: PageDefinition }> = [
108
- {
109
- module: 'sales',
110
- page: {
111
- key: 'sales.broken',
112
- label: 'Broken',
113
- type: 'collection',
114
- body: [{ type: 'data', source: { model: 'sales.nonexistent' }, children: [] }],
115
- },
116
- },
117
- ];
118
-
119
- const warnings = validatePageSources(pages, knownModels);
120
- expect(warnings).toHaveLength(1);
121
- expect(warnings[0].pageKey).toBe('sales.broken');
122
- expect(warnings[0].location).toBe('body[0]');
123
- expect(warnings[0].message).toContain('sales.nonexistent');
124
- });
125
-
126
- it('warns when nested widget references non-existent model', () => {
127
- const pages: Array<{ module: string; page: PageDefinition }> = [
128
- {
129
- module: 'sales',
130
- page: {
131
- key: 'sales.detail',
132
- label: 'Detail',
133
- type: 'collection',
134
- body: [
135
- {
136
- type: 'data',
137
- source: { model: 'sales.order' },
138
- children: [{ type: 'data', source: { model: 'contacts.missing' }, children: [] }],
139
- },
140
- ],
141
- },
142
- },
143
- ];
144
-
145
- const warnings = validatePageSources(pages, knownModels);
146
- expect(warnings).toHaveLength(1);
147
- expect(warnings[0].location).toBe('body[0].children[0]');
148
- expect(warnings[0].message).toContain('contacts.missing');
149
- });
150
-
151
- it('no warnings for widgets without model sources', () => {
152
- const pages: Array<{ module: string; page: PageDefinition }> = [
153
- {
154
- module: 'reports',
155
- page: {
156
- key: 'reports.revenue',
157
- label: 'Revenue',
158
- type: 'dashboard',
159
- body: [
160
- { type: 'text', props: { content: 'Revenue Report' } },
161
- { type: 'button', props: { label: 'Refresh' } },
162
- ],
163
- },
164
- },
165
- ];
166
-
167
- const warnings = validatePageSources(pages, knownModels);
168
- expect(warnings).toHaveLength(0);
169
- });
170
- });