@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,175 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- ServiceRegistry,
4
- ServiceCircularDependencyError,
5
- ServiceNotFoundError,
6
- DuplicateServiceError,
7
- } from '../registry.js';
8
- import type { ServiceDefinition, ServiceContext } from '../types.js';
9
-
10
- const baseContext: Omit<ServiceContext, 'service'> = {
11
- db: {} as ServiceContext['db'],
12
- schema: {} as ServiceContext['schema'],
13
- enqueue: async () => {},
14
- events: { emit: async () => {}, on: () => {} },
15
- config: {},
16
- };
17
-
18
- describe('ServiceRegistry', () => {
19
- it('registers and retrieves a service', () => {
20
- const registry = new ServiceRegistry();
21
- const def: ServiceDefinition = {
22
- name: 'notifications',
23
- factory: () => ({
24
- send: async (msg: unknown) => msg,
25
- }),
26
- };
27
-
28
- registry.register(def);
29
- expect(registry.has('notifications')).toBe(true);
30
-
31
- const instance = registry.get('notifications', baseContext);
32
- expect(instance.send).toBeTypeOf('function');
33
- });
34
-
35
- it('returns the same instance on repeated get (lazy singleton)', () => {
36
- const registry = new ServiceRegistry();
37
- let callCount = 0;
38
- registry.register({
39
- name: 'counter',
40
- factory: () => {
41
- callCount++;
42
- return { count: () => callCount };
43
- },
44
- });
45
-
46
- const a = registry.get('counter', baseContext);
47
- const b = registry.get('counter', baseContext);
48
- expect(a).toBe(b);
49
- expect(callCount).toBe(1);
50
- });
51
-
52
- it('throws on duplicate registration', () => {
53
- const registry = new ServiceRegistry();
54
- registry.register({ name: 'svc', factory: () => ({}) });
55
-
56
- expect(() => registry.register({ name: 'svc', factory: () => ({}) })).toThrow(
57
- DuplicateServiceError,
58
- );
59
- });
60
-
61
- it('throws when getting an unregistered service', () => {
62
- const registry = new ServiceRegistry();
63
-
64
- expect(() => registry.get('nonexistent', baseContext)).toThrow(ServiceNotFoundError);
65
- });
66
-
67
- it('detects circular dependencies at registration time', () => {
68
- const registry = new ServiceRegistry();
69
- registry.register({ name: 'a', deps: ['b'], factory: () => ({}) });
70
- registry.register({ name: 'b', deps: ['c'], factory: () => ({}) });
71
- registry.register({ name: 'c', deps: ['a'], factory: () => ({}) });
72
-
73
- expect(() => registry.detectCircularDependencies()).toThrow(ServiceCircularDependencyError);
74
- });
75
-
76
- it('does not throw for valid dependency graph', () => {
77
- const registry = new ServiceRegistry();
78
- registry.register({ name: 'a', deps: ['b'], factory: () => ({}) });
79
- registry.register({ name: 'b', deps: ['c'], factory: () => ({}) });
80
- registry.register({ name: 'c', factory: () => ({}) });
81
-
82
- expect(() => registry.detectCircularDependencies()).not.toThrow();
83
- });
84
-
85
- it('injects dependencies via service() in context', () => {
86
- const registry = new ServiceRegistry();
87
- registry.register({
88
- name: 'logger',
89
- factory: () => ({ log: (msg: unknown) => `logged: ${msg}` }),
90
- });
91
- registry.register({
92
- name: 'mailer',
93
- deps: ['logger'],
94
- factory: (ctx: ServiceContext) => ({
95
- send: (to: unknown) => {
96
- const logger = ctx.service('logger');
97
- return (logger as any).log(`email to ${to}`);
98
- },
99
- }),
100
- });
101
-
102
- const mailer = registry.get('mailer', baseContext);
103
- expect(mailer.send('user@test.com')).toBe('logged: email to user@test.com');
104
- });
105
-
106
- it('resolves deep dependency chains', () => {
107
- const registry = new ServiceRegistry();
108
- registry.register({
109
- name: 'c',
110
- factory: () => ({ value: () => 42 }),
111
- });
112
- registry.register({
113
- name: 'b',
114
- deps: ['c'],
115
- factory: (ctx) => ({ value: () => (ctx.service('c') as any).value() + 1 }),
116
- });
117
- registry.register({
118
- name: 'a',
119
- deps: ['b'],
120
- factory: (ctx) => ({ value: () => (ctx.service('b') as any).value() + 1 }),
121
- });
122
-
123
- const a = registry.get('a', baseContext);
124
- expect(a.value()).toBe(44);
125
- });
126
-
127
- it('detects circular dependency at runtime during resolution', () => {
128
- const registry = new ServiceRegistry();
129
- registry.register({
130
- name: 'x',
131
- deps: ['y'],
132
- factory: (ctx) => {
133
- ctx.service('y');
134
- return {};
135
- },
136
- });
137
- registry.register({
138
- name: 'y',
139
- deps: ['x'],
140
- factory: (ctx) => {
141
- ctx.service('x');
142
- return {};
143
- },
144
- });
145
-
146
- expect(() => registry.get('x', baseContext)).toThrow(ServiceCircularDependencyError);
147
- });
148
-
149
- it('reset clears cached instances', () => {
150
- const registry = new ServiceRegistry();
151
- let callCount = 0;
152
- registry.register({
153
- name: 'svc',
154
- factory: () => {
155
- callCount++;
156
- return {};
157
- },
158
- });
159
-
160
- registry.get('svc', baseContext);
161
- expect(callCount).toBe(1);
162
-
163
- registry.reset();
164
- registry.get('svc', baseContext);
165
- expect(callCount).toBe(2);
166
- });
167
-
168
- it('getAll returns all definitions', () => {
169
- const registry = new ServiceRegistry();
170
- registry.register({ name: 'a', factory: () => ({}) });
171
- registry.register({ name: 'b', factory: () => ({}) });
172
-
173
- expect(registry.getAll()).toHaveLength(2);
174
- });
175
- });
@@ -1,13 +0,0 @@
1
- export {
2
- ServiceRegistry,
3
- ServiceCircularDependencyError,
4
- ServiceNotFoundError,
5
- DuplicateServiceError,
6
- } from './registry.js';
7
- export type {
8
- ServiceDefinition,
9
- ServiceFactory,
10
- ServiceInstance,
11
- ServiceDependency,
12
- ServiceContext,
13
- } from './types.js';
@@ -1,156 +0,0 @@
1
- import type { ServiceDefinition, ServiceInstance, ServiceContext } from './types.js';
2
-
3
- // --- Error types ---
4
-
5
- export class ServiceCircularDependencyError extends Error {
6
- constructor(public readonly cycle: string[]) {
7
- super(`Circular service dependency detected: ${cycle.join(' → ')}`);
8
- this.name = 'ServiceCircularDependencyError';
9
- }
10
- }
11
-
12
- export class ServiceNotFoundError extends Error {
13
- constructor(public readonly serviceName: string) {
14
- super(`Service "${serviceName}" is not registered`);
15
- this.name = 'ServiceNotFoundError';
16
- }
17
- }
18
-
19
- export class DuplicateServiceError extends Error {
20
- constructor(public readonly serviceName: string) {
21
- super(`Service "${serviceName}" is already registered`);
22
- this.name = 'DuplicateServiceError';
23
- }
24
- }
25
-
26
- // --- Registry ---
27
-
28
- /**
29
- * Manages service definitions and lazily-created singleton instances.
30
- * Services declare dependencies by name; the registry resolves them on first access.
31
- */
32
- export class ServiceRegistry {
33
- /** Registered service blueprints (name -> definition). */
34
- private definitions = new Map<string, ServiceDefinition>();
35
-
36
- /** Cached singleton instances, created on first `get()` call. */
37
- private instances = new Map<string, ServiceInstance>();
38
-
39
- /** Tracks which services are mid-resolution to detect runtime cycles. */
40
- private resolving = new Set<string>();
41
-
42
- // --- Registration & lookup ---
43
-
44
- /** Register a service definition. Throws if the name is already taken. */
45
- register(definition: ServiceDefinition): void {
46
- if (this.definitions.has(definition.name)) {
47
- throw new DuplicateServiceError(definition.name);
48
- }
49
- this.definitions.set(definition.name, definition);
50
- }
51
-
52
- /** Check whether a service name has been registered. */
53
- has(name: string): boolean {
54
- return this.definitions.has(name);
55
- }
56
-
57
- /** Return the raw definition for a service, or undefined if not registered. */
58
- getDefinition(name: string): ServiceDefinition | undefined {
59
- return this.definitions.get(name);
60
- }
61
-
62
- /** Return all registered definitions. */
63
- getAll(): ServiceDefinition[] {
64
- return [...this.definitions.values()];
65
- }
66
-
67
- // --- Dependency validation ---
68
-
69
- /**
70
- * Walk the full dependency graph up-front and throw if any cycle exists.
71
- * Uses depth-first traversal with a recursion stack to detect back-edges.
72
- */
73
- detectCircularDependencies(): void {
74
- const fullyVisited = new Set<string>();
75
- const currentPath = new Set<string>();
76
-
77
- const visit = (serviceName: string, ancestors: string[]): void => {
78
- // Back-edge: we've looped back to a node already on the current path.
79
- if (currentPath.has(serviceName)) {
80
- const cycleStart = ancestors.indexOf(serviceName);
81
- throw new ServiceCircularDependencyError([...ancestors.slice(cycleStart), serviceName]);
82
- }
83
-
84
- // Already fully explored from a previous traversal root -- skip.
85
- if (fullyVisited.has(serviceName)) return;
86
-
87
- currentPath.add(serviceName);
88
- ancestors.push(serviceName);
89
-
90
- const definition = this.definitions.get(serviceName);
91
- if (definition?.deps) {
92
- for (const dependency of definition.deps) {
93
- visit(dependency, ancestors);
94
- }
95
- }
96
-
97
- ancestors.pop();
98
- currentPath.delete(serviceName);
99
- fullyVisited.add(serviceName);
100
- };
101
-
102
- for (const serviceName of this.definitions.keys()) {
103
- visit(serviceName, []);
104
- }
105
- }
106
-
107
- // --- Resolution ---
108
-
109
- /**
110
- * Resolve a service by name: return the cached instance or create it via its factory.
111
- * Dependencies are resolved recursively through the same mechanism.
112
- */
113
- get(name: string, baseContext: Omit<ServiceContext, 'service'>): ServiceInstance {
114
- // Return cached singleton if already instantiated.
115
- const cached = this.instances.get(name);
116
- if (cached) return cached;
117
-
118
- const definition = this.definitions.get(name);
119
- if (!definition) {
120
- throw new ServiceNotFoundError(name);
121
- }
122
-
123
- // Guard against runtime circular resolution (e.g. A -> B -> A).
124
- if (this.resolving.has(name)) {
125
- throw new ServiceCircularDependencyError([...this.resolving, name]);
126
- }
127
-
128
- this.resolving.add(name);
129
- try {
130
- const instance = this.createInstance(definition, baseContext);
131
- this.instances.set(name, instance);
132
- return instance;
133
- } finally {
134
- this.resolving.delete(name);
135
- }
136
- }
137
-
138
- /** Discard all cached instances (definitions remain registered). */
139
- reset(): void {
140
- this.instances.clear();
141
- }
142
-
143
- // --- Private helpers ---
144
-
145
- /** Invoke a service factory with a context that can resolve sibling services. */
146
- private createInstance(
147
- definition: ServiceDefinition,
148
- baseContext: Omit<ServiceContext, 'service'>,
149
- ): ServiceInstance {
150
- const context: ServiceContext = {
151
- ...baseContext,
152
- service: (dependencyName: string) => this.get(dependencyName, baseContext),
153
- };
154
- return definition.factory(context);
155
- }
156
- }
@@ -1,27 +0,0 @@
1
- import type { FrameworkContext, ServiceInstance } from '@rangka/shared';
2
-
3
- export interface ServiceDependency {
4
- name: string;
5
- }
6
-
7
- export interface ServiceDefinition {
8
- name: string;
9
- deps?: string[];
10
- factory: ServiceFactory;
11
- }
12
-
13
- export type ServiceFactory = (ctx: ServiceContext) => ServiceInstance;
14
-
15
- export type { ServiceInstance };
16
-
17
- export interface ServiceContext {
18
- db: FrameworkContext['db'];
19
- schema: FrameworkContext['schema'];
20
- enqueue: FrameworkContext['enqueue'];
21
- events: FrameworkContext['events'];
22
- config: FrameworkContext['config'];
23
- models?: FrameworkContext['models'];
24
- auth?: FrameworkContext['auth'];
25
- scope?: FrameworkContext['scope'];
26
- service: (name: string) => ServiceInstance;
27
- }
@@ -1,195 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { validateFields } from '../field-validator.js';
3
- import type { ResolvedModel, ResolvedField } from '../../schema/types.js';
4
- import type { FieldConfig } from '@rangka/shared';
5
-
6
- function makeModel(fields: ResolvedField[]): ResolvedModel {
7
- return {
8
- app: 'test',
9
- module: 'test',
10
- name: 'item',
11
- qualifiedName: 'test.item',
12
- auditLog: false,
13
- traits: [],
14
- fields,
15
- indexes: [],
16
- };
17
- }
18
-
19
- function field(name: string, config: Partial<FieldConfig> & { type: string }): ResolvedField {
20
- return { name, config: config as FieldConfig, provenance: { source: 'base' } };
21
- }
22
-
23
- describe('validateFields', () => {
24
- describe('minLength / maxLength', () => {
25
- const model = makeModel([
26
- field('name', { type: 'string', validation: { minLength: 3, maxLength: 50 } }),
27
- ]);
28
-
29
- it('passes when string length is within bounds', () => {
30
- expect(validateFields(model, { name: 'hello' }, 'create')).toEqual([]);
31
- });
32
-
33
- it('fails when string is too short', () => {
34
- const result = validateFields(model, { name: 'ab' }, 'create');
35
- expect(result).toHaveLength(1);
36
- expect(result[0].field).toBe('name');
37
- expect(result[0].rule).toBe('minLength');
38
- });
39
-
40
- it('fails when string is too long', () => {
41
- const result = validateFields(model, { name: 'a'.repeat(51) }, 'create');
42
- expect(result).toHaveLength(1);
43
- expect(result[0].field).toBe('name');
44
- expect(result[0].rule).toBe('maxLength');
45
- });
46
- });
47
-
48
- describe('min / max', () => {
49
- const model = makeModel([field('quantity', { type: 'int', validation: { min: 0, max: 100 } })]);
50
-
51
- it('passes when number is within bounds', () => {
52
- expect(validateFields(model, { quantity: 50 }, 'create')).toEqual([]);
53
- });
54
-
55
- it('fails when number is below min', () => {
56
- const result = validateFields(model, { quantity: -1 }, 'create');
57
- expect(result).toHaveLength(1);
58
- expect(result[0].field).toBe('quantity');
59
- expect(result[0].rule).toBe('min');
60
- });
61
-
62
- it('fails when number is above max', () => {
63
- const result = validateFields(model, { quantity: 101 }, 'create');
64
- expect(result).toHaveLength(1);
65
- expect(result[0].field).toBe('quantity');
66
- expect(result[0].rule).toBe('max');
67
- });
68
-
69
- it('passes at exact boundaries', () => {
70
- expect(validateFields(model, { quantity: 0 }, 'create')).toEqual([]);
71
- expect(validateFields(model, { quantity: 100 }, 'create')).toEqual([]);
72
- });
73
- });
74
-
75
- describe('pattern', () => {
76
- const model = makeModel([
77
- field('code', { type: 'string', validation: { pattern: '^[A-Z]{3}-\\d+$' } }),
78
- ]);
79
-
80
- it('passes when value matches pattern', () => {
81
- expect(validateFields(model, { code: 'ABC-123' }, 'create')).toEqual([]);
82
- });
83
-
84
- it('fails when value does not match pattern', () => {
85
- const result = validateFields(model, { code: 'abc-123' }, 'create');
86
- expect(result).toHaveLength(1);
87
- expect(result[0].field).toBe('code');
88
- expect(result[0].rule).toBe('pattern');
89
- });
90
- });
91
-
92
- describe('format', () => {
93
- const model = makeModel([
94
- field('email', { type: 'string', validation: { format: 'email' } }),
95
- field('website', { type: 'string', validation: { format: 'url' } }),
96
- field('ref', { type: 'string', validation: { format: 'uuid' } }),
97
- ]);
98
-
99
- it('passes valid email', () => {
100
- expect(validateFields(model, { email: 'user@example.com' }, 'create')).toEqual([]);
101
- });
102
-
103
- it('fails invalid email', () => {
104
- const result = validateFields(model, { email: 'not-an-email' }, 'create');
105
- expect(result).toHaveLength(1);
106
- expect(result[0].rule).toBe('format');
107
- });
108
-
109
- it('passes valid url', () => {
110
- expect(validateFields(model, { website: 'https://example.com' }, 'create')).toEqual([]);
111
- });
112
-
113
- it('fails invalid url', () => {
114
- const result = validateFields(model, { website: 'not-a-url' }, 'create');
115
- expect(result).toHaveLength(1);
116
- expect(result[0].rule).toBe('format');
117
- });
118
-
119
- it('passes valid uuid', () => {
120
- expect(
121
- validateFields(model, { ref: '550e8400-e29b-41d4-a716-446655440000' }, 'create'),
122
- ).toEqual([]);
123
- });
124
-
125
- it('fails invalid uuid', () => {
126
- const result = validateFields(model, { ref: 'not-a-uuid' }, 'create');
127
- expect(result).toHaveLength(1);
128
- expect(result[0].rule).toBe('format');
129
- });
130
-
131
- it('ignores unknown format', () => {
132
- const m = makeModel([
133
- field('x', { type: 'string', validation: { format: 'unknown_format' } }),
134
- ]);
135
- expect(validateFields(m, { x: 'anything' }, 'create')).toEqual([]);
136
- });
137
- });
138
-
139
- describe('custom message', () => {
140
- it('uses custom message when provided', () => {
141
- const model = makeModel([
142
- field('age', {
143
- type: 'int',
144
- validation: { min: 18, message: 'Must be at least 18 years old' },
145
- }),
146
- ]);
147
- const result = validateFields(model, { age: 10 }, 'create');
148
- expect(result[0].message).toBe('Must be at least 18 years old');
149
- });
150
- });
151
-
152
- describe('update operation', () => {
153
- const model = makeModel([
154
- field('name', { type: 'string', validation: { minLength: 3 } }),
155
- field('quantity', { type: 'int', validation: { min: 0 } }),
156
- ]);
157
-
158
- it('only validates fields present in body', () => {
159
- expect(validateFields(model, { quantity: 5 }, 'update')).toEqual([]);
160
- });
161
-
162
- it('validates fields that are present', () => {
163
- const result = validateFields(model, { name: 'ab' }, 'update');
164
- expect(result).toHaveLength(1);
165
- expect(result[0].field).toBe('name');
166
- });
167
-
168
- it('skips null/undefined fields', () => {
169
- expect(validateFields(model, { name: undefined }, 'update')).toEqual([]);
170
- expect(validateFields(model, { name: null }, 'update')).toEqual([]);
171
- });
172
- });
173
-
174
- describe('fields without validation config', () => {
175
- const model = makeModel([field('title', { type: 'string' }), field('count', { type: 'int' })]);
176
-
177
- it('skips fields without validation', () => {
178
- expect(validateFields(model, { title: '', count: -999 }, 'create')).toEqual([]);
179
- });
180
- });
181
-
182
- describe('multiple violations', () => {
183
- const model = makeModel([
184
- field('name', { type: 'string', validation: { minLength: 3 } }),
185
- field('quantity', { type: 'int', validation: { min: 0 } }),
186
- ]);
187
-
188
- it('returns all violations at once', () => {
189
- const result = validateFields(model, { name: 'ab', quantity: -1 }, 'create');
190
- expect(result).toHaveLength(2);
191
- expect(result.map((v) => v.field)).toContain('name');
192
- expect(result.map((v) => v.field)).toContain('quantity');
193
- });
194
- });
195
- });
@@ -1,113 +0,0 @@
1
- import type { ResolvedModel } from '../schema/types.js';
2
- import type { ValidationConfig } from '@rangka/shared';
3
- import { isNil } from '../helpers/coerce.js';
4
-
5
- export interface FieldViolation {
6
- field: string;
7
- rule: string;
8
- message: string;
9
- }
10
-
11
- const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
12
- const URL_RE = /^https?:\/\/.+/;
13
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
-
15
- const FORMAT_VALIDATORS: Record<string, (value: string) => boolean> = {
16
- email: (v) => EMAIL_RE.test(v),
17
- url: (v) => URL_RE.test(v),
18
- uuid: (v) => UUID_RE.test(v),
19
- };
20
-
21
- export function validateFields(
22
- model: ResolvedModel,
23
- body: Record<string, unknown>,
24
- operation: 'create' | 'update',
25
- ): FieldViolation[] {
26
- const violations: FieldViolation[] = [];
27
-
28
- for (const field of model.fields) {
29
- if (!('validation' in field.config)) continue;
30
- const validation = (field.config as { validation?: ValidationConfig }).validation;
31
- if (!validation) continue;
32
-
33
- const value = body[field.name];
34
-
35
- if (isNil(value)) {
36
- if (operation === 'update') continue;
37
- continue;
38
- }
39
-
40
- const fieldViolations = validateValue(field.name, value, validation);
41
- violations.push(...fieldViolations);
42
- }
43
-
44
- return violations;
45
- }
46
-
47
- function validateValue(
48
- fieldName: string,
49
- value: unknown,
50
- config: ValidationConfig,
51
- ): FieldViolation[] {
52
- const violations: FieldViolation[] = [];
53
-
54
- if (typeof value === 'string') {
55
- if (config.minLength !== undefined && value.length < config.minLength) {
56
- violations.push({
57
- field: fieldName,
58
- rule: 'minLength',
59
- message: config.message ?? `${fieldName} must be at least ${config.minLength} characters`,
60
- });
61
- }
62
-
63
- if (config.maxLength !== undefined && value.length > config.maxLength) {
64
- violations.push({
65
- field: fieldName,
66
- rule: 'maxLength',
67
- message: config.message ?? `${fieldName} must be at most ${config.maxLength} characters`,
68
- });
69
- }
70
-
71
- if (config.pattern !== undefined) {
72
- const re = new RegExp(config.pattern);
73
- if (!re.test(value)) {
74
- violations.push({
75
- field: fieldName,
76
- rule: 'pattern',
77
- message: config.message ?? `${fieldName} does not match the required pattern`,
78
- });
79
- }
80
- }
81
-
82
- if (config.format !== undefined) {
83
- const validator = FORMAT_VALIDATORS[config.format];
84
- if (validator && !validator(value)) {
85
- violations.push({
86
- field: fieldName,
87
- rule: 'format',
88
- message: config.message ?? `${fieldName} must be a valid ${config.format}`,
89
- });
90
- }
91
- }
92
- }
93
-
94
- if (typeof value === 'number') {
95
- if (config.min !== undefined && value < config.min) {
96
- violations.push({
97
- field: fieldName,
98
- rule: 'min',
99
- message: config.message ?? `${fieldName} must be at least ${config.min}`,
100
- });
101
- }
102
-
103
- if (config.max !== undefined && value > config.max) {
104
- violations.push({
105
- field: fieldName,
106
- rule: 'max',
107
- message: config.message ?? `${fieldName} must be at most ${config.max}`,
108
- });
109
- }
110
- }
111
-
112
- return violations;
113
- }
@@ -1 +0,0 @@
1
- export { validateFields, type FieldViolation } from './field-validator.js';
@@ -1,3 +0,0 @@
1
- export { WidgetRegistry } from './widget-registry.js';
2
- export { validatePageBody } from './slot-validator.js';
3
- export type { SlotValidationError } from './slot-validator.js';