@rangka/core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/package.json +6 -2
  2. package/.claude/skills/extend-core/SKILL.md +0 -133
  3. package/.turbo/turbo-build.log +0 -4
  4. package/CHANGELOG.md +0 -18
  5. package/CLAUDE.md +0 -180
  6. package/src/__tests__/coerce.test.ts +0 -154
  7. package/src/__tests__/context.test.ts +0 -111
  8. package/src/__tests__/helpers.ts +0 -21
  9. package/src/__tests__/index.test.ts +0 -7
  10. package/src/__tests__/widgets.test.ts +0 -197
  11. package/src/api/__tests__/handlers.test.ts +0 -389
  12. package/src/api/__tests__/include-resolver.test.ts +0 -393
  13. package/src/api/__tests__/middleware.test.ts +0 -100
  14. package/src/api/__tests__/openapi-schema.test.ts +0 -210
  15. package/src/api/__tests__/query-parser.test.ts +0 -291
  16. package/src/api/__tests__/route-generator.test.ts +0 -137
  17. package/src/api/__tests__/server.test.ts +0 -73
  18. package/src/api/__tests__/swagger.test.ts +0 -166
  19. package/src/api/handlers.ts +0 -274
  20. package/src/api/include-resolver.ts +0 -27
  21. package/src/api/index.ts +0 -4
  22. package/src/api/meta-handler.ts +0 -254
  23. package/src/api/openapi-schema.ts +0 -99
  24. package/src/api/query-parser.ts +0 -315
  25. package/src/api/route-generator.ts +0 -448
  26. package/src/api/server.ts +0 -147
  27. package/src/api/types.ts +0 -16
  28. package/src/audit/__tests__/audit.test.ts +0 -144
  29. package/src/audit/index.ts +0 -3
  30. package/src/audit/record.ts +0 -69
  31. package/src/audit/tables.ts +0 -48
  32. package/src/audit/types.ts +0 -26
  33. package/src/auth/__tests__/core-module.test.ts +0 -54
  34. package/src/auth/__tests__/debug.test.ts +0 -47
  35. package/src/auth/__tests__/field-permissions.test.ts +0 -245
  36. package/src/auth/__tests__/integration.test.ts +0 -208
  37. package/src/auth/__tests__/meta-boot.test.ts +0 -538
  38. package/src/auth/__tests__/model-permissions.test.ts +0 -205
  39. package/src/auth/__tests__/password.test.ts +0 -29
  40. package/src/auth/__tests__/permission-registry.test.ts +0 -313
  41. package/src/auth/__tests__/scope-hook.test.ts +0 -509
  42. package/src/auth/__tests__/scope-registry.test.ts +0 -297
  43. package/src/auth/__tests__/scopes.test.ts +0 -66
  44. package/src/auth/__tests__/session.test.ts +0 -214
  45. package/src/auth/core-models.ts +0 -52
  46. package/src/auth/core-module.ts +0 -59
  47. package/src/auth/debug.ts +0 -157
  48. package/src/auth/field-permissions.ts +0 -116
  49. package/src/auth/index.ts +0 -37
  50. package/src/auth/model-permissions.ts +0 -59
  51. package/src/auth/password.ts +0 -22
  52. package/src/auth/permission-registry.ts +0 -171
  53. package/src/auth/scope-filters.ts +0 -11
  54. package/src/auth/scope-registry.ts +0 -121
  55. package/src/auth/scopes.ts +0 -146
  56. package/src/auth/seed.ts +0 -44
  57. package/src/auth/session.ts +0 -178
  58. package/src/auth/types.ts +0 -50
  59. package/src/boot/__tests__/page-scanning.test.ts +0 -170
  60. package/src/boot/__tests__/page-utils.test.ts +0 -225
  61. package/src/boot/__tests__/project-scanner.test.ts +0 -88
  62. package/src/boot/dependency-sort.ts +0 -82
  63. package/src/boot/discovery.ts +0 -85
  64. package/src/boot/index.ts +0 -457
  65. package/src/boot/page-utils.ts +0 -110
  66. package/src/boot/project-scanner.ts +0 -397
  67. package/src/boot/schema-loader.ts +0 -26
  68. package/src/boot/schema-merger.ts +0 -125
  69. package/src/boot/traits.ts +0 -25
  70. package/src/boot/types.ts +0 -73
  71. package/src/context.ts +0 -105
  72. package/src/db/__tests__/cascade-delete.test.ts +0 -182
  73. package/src/db/__tests__/desired-state.test.ts +0 -136
  74. package/src/db/__tests__/diff-engine.test.ts +0 -635
  75. package/src/db/__tests__/field-mapper.test.ts +0 -355
  76. package/src/db/__tests__/introspect.test.ts +0 -70
  77. package/src/db/__tests__/search-filter.test.ts +0 -45
  78. package/src/db/__tests__/sequence.test.ts +0 -221
  79. package/src/db/auto-sync.ts +0 -133
  80. package/src/db/client.ts +0 -147
  81. package/src/db/desired-state.ts +0 -98
  82. package/src/db/diff-engine.ts +0 -305
  83. package/src/db/field-mapper.ts +0 -504
  84. package/src/db/filter-applier.ts +0 -89
  85. package/src/db/include-resolver.ts +0 -40
  86. package/src/db/index.ts +0 -23
  87. package/src/db/introspect.ts +0 -265
  88. package/src/db/model-include-resolver.ts +0 -327
  89. package/src/db/model-ops.ts +0 -281
  90. package/src/db/scope-enforcer.ts +0 -37
  91. package/src/db/types.ts +0 -98
  92. package/src/errors.ts +0 -41
  93. package/src/events/__tests__/bus.test.ts +0 -105
  94. package/src/events/bus.ts +0 -89
  95. package/src/events/index.ts +0 -2
  96. package/src/events/types.ts +0 -9
  97. package/src/external-model/__tests__/computed-fields.test.ts +0 -106
  98. package/src/external-model/__tests__/field-mapper.test.ts +0 -160
  99. package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
  100. package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
  101. package/src/external-model/__tests__/query-executor.test.ts +0 -284
  102. package/src/external-model/__tests__/schema-converter.test.ts +0 -174
  103. package/src/external-model/computed-fields.ts +0 -15
  104. package/src/external-model/define.ts +0 -5
  105. package/src/external-model/external-model-ops.ts +0 -108
  106. package/src/external-model/field-mapper.ts +0 -66
  107. package/src/external-model/in-memory-ops.ts +0 -107
  108. package/src/external-model/index.ts +0 -7
  109. package/src/external-model/mutation-executor.ts +0 -71
  110. package/src/external-model/query-executor.ts +0 -100
  111. package/src/external-model/schema-converter.ts +0 -53
  112. package/src/external-model/types.ts +0 -32
  113. package/src/fixtures/__tests__/fixtures.test.ts +0 -203
  114. package/src/fixtures/index.ts +0 -10
  115. package/src/fixtures/loader.ts +0 -196
  116. package/src/fixtures/registry.ts +0 -125
  117. package/src/fixtures/types.ts +0 -33
  118. package/src/helpers/assert-ownership.ts +0 -19
  119. package/src/helpers/coerce.ts +0 -28
  120. package/src/helpers/stamping.ts +0 -28
  121. package/src/helpers/validation.ts +0 -14
  122. package/src/hooks/__tests__/context.test.ts +0 -73
  123. package/src/hooks/__tests__/executor.test.ts +0 -433
  124. package/src/hooks/__tests__/middleware.test.ts +0 -224
  125. package/src/hooks/__tests__/registry.test.ts +0 -50
  126. package/src/hooks/context.ts +0 -89
  127. package/src/hooks/errors.ts +0 -11
  128. package/src/hooks/executor.ts +0 -115
  129. package/src/hooks/index.ts +0 -10
  130. package/src/hooks/middleware.ts +0 -220
  131. package/src/hooks/registry.ts +0 -20
  132. package/src/hooks/types.ts +0 -32
  133. package/src/index.ts +0 -172
  134. package/src/jobs/__tests__/enqueue.test.ts +0 -77
  135. package/src/jobs/__tests__/integration.test.ts +0 -71
  136. package/src/jobs/__tests__/registry.test.ts +0 -103
  137. package/src/jobs/__tests__/scheduler.test.ts +0 -92
  138. package/src/jobs/__tests__/worker-execution.test.ts +0 -202
  139. package/src/jobs/__tests__/worker.test.ts +0 -119
  140. package/src/jobs/enqueue.ts +0 -93
  141. package/src/jobs/index.ts +0 -14
  142. package/src/jobs/registry.ts +0 -92
  143. package/src/jobs/scheduler.ts +0 -205
  144. package/src/jobs/tables.ts +0 -132
  145. package/src/jobs/types.ts +0 -62
  146. package/src/jobs/worker.ts +0 -272
  147. package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
  148. package/src/model-api/__tests__/extended-api.test.ts +0 -244
  149. package/src/model-api/__tests__/filter-applier.test.ts +0 -177
  150. package/src/model-api/__tests__/filter-translator.test.ts +0 -186
  151. package/src/model-api/__tests__/include-resolver.test.ts +0 -226
  152. package/src/model-api/__tests__/model-access.test.ts +0 -284
  153. package/src/model-api/__tests__/query-builder.test.ts +0 -224
  154. package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
  155. package/src/model-api/field-access.ts +0 -28
  156. package/src/model-api/filter-applier.ts +0 -1
  157. package/src/model-api/filter-translator.ts +0 -67
  158. package/src/model-api/include-resolver.ts +0 -2
  159. package/src/model-api/index.ts +0 -86
  160. package/src/model-api/query-builder.ts +0 -155
  161. package/src/model-api/scope-enforcer.ts +0 -3
  162. package/src/model-api/types.ts +0 -139
  163. package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
  164. package/src/plugins/__tests__/lifecycle.test.ts +0 -96
  165. package/src/plugins/__tests__/loader.test.ts +0 -273
  166. package/src/plugins/__tests__/validator.test.ts +0 -275
  167. package/src/plugins/adapter-registry.ts +0 -42
  168. package/src/plugins/define.ts +0 -5
  169. package/src/plugins/index.ts +0 -28
  170. package/src/plugins/lifecycle.ts +0 -27
  171. package/src/plugins/loader.ts +0 -126
  172. package/src/plugins/types.ts +0 -76
  173. package/src/plugins/validator.ts +0 -141
  174. package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
  175. package/src/schema/registry.ts +0 -93
  176. package/src/schema/relationships.ts +0 -93
  177. package/src/schema/types.ts +0 -43
  178. package/src/services/__tests__/integration.test.ts +0 -63
  179. package/src/services/__tests__/registry.test.ts +0 -175
  180. package/src/services/index.ts +0 -13
  181. package/src/services/registry.ts +0 -156
  182. package/src/services/types.ts +0 -27
  183. package/src/validation/__tests__/field-validator.test.ts +0 -195
  184. package/src/validation/field-validator.ts +0 -113
  185. package/src/validation/index.ts +0 -1
  186. package/src/widgets/index.ts +0 -3
  187. package/src/widgets/slot-validator.ts +0 -87
  188. package/src/widgets/widget-registry.ts +0 -32
  189. package/tests/boot.test.ts +0 -323
  190. package/tests/dependency-sort.test.ts +0 -99
  191. package/tests/discovery.test.ts +0 -126
  192. package/tests/registry.test.ts +0 -216
  193. package/tests/schema-loader.test.ts +0 -52
  194. package/tests/schema-merger.test.ts +0 -180
  195. package/tsconfig.json +0 -9
  196. package/tsconfig.tsbuildinfo +0 -1
  197. package/vitest.config.ts +0 -14
@@ -1,281 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { sql, type Kysely } from 'kysely';
3
- import type { ResolvedModel } from '../schema/types.js';
4
- import type { SchemaRegistry } from '../schema/registry.js';
5
- import type { RequestContext } from '../auth/types.js';
6
- import type { ModelOps, QueryState, QueryResult, QueryResultWithMeta } from '../model-api/types.js';
7
- import { applyModelFilters, applySearchFilter } from './filter-applier.js';
8
- import { applyScopeEnforcement } from './scope-enforcer.js';
9
- import { modelToTableName } from './field-mapper.js';
10
- import { toCount } from '../helpers/coerce.js';
11
- import { NotFoundError } from '../errors.js';
12
-
13
- interface DbLike {
14
- selectFrom(table: string): any;
15
- insertInto(table: string): any;
16
- updateTable(table: string): any;
17
- deleteFrom(table: string): any;
18
- }
19
-
20
- export interface KyselyModelOpsConfig {
21
- db: Kysely<any> | DbLike;
22
- model: ResolvedModel;
23
- registry: SchemaRegistry;
24
- auth?: RequestContext;
25
- tableName?: string;
26
- }
27
-
28
- export class KyselyModelOps implements ModelOps {
29
- private readonly db: DbLike;
30
- private readonly model: ResolvedModel;
31
- private readonly registry: SchemaRegistry;
32
- private readonly defaultAuth?: RequestContext;
33
- private readonly tableName: string;
34
-
35
- constructor(config: KyselyModelOpsConfig) {
36
- this.db = config.db as DbLike;
37
- this.model = config.model;
38
- this.registry = config.registry;
39
- this.defaultAuth = config.auth;
40
- this.tableName = config.tableName ?? config.model.qualifiedName;
41
- }
42
-
43
- async find(state: QueryState): Promise<QueryResult> {
44
- let query = this.buildBaseQuery(state);
45
- query = this.applySelect(query, state);
46
- query = this.applySorts(query, state);
47
- query = this.applyPagination(query, state);
48
- const data = await query.execute();
49
- return { data: data as Record<string, unknown>[] };
50
- }
51
-
52
- async findWithMeta(state: QueryState): Promise<QueryResultWithMeta> {
53
- const limit = state.limitVal ?? 25;
54
- const page = state.pageVal ?? 1;
55
- const offset = state.offsetVal ?? (page - 1) * limit;
56
-
57
- let countQuery = this.buildBaseQuery(state);
58
- countQuery = countQuery.select(sql`count(*)`.as('count'));
59
- const countResult = await countQuery.executeTakeFirst();
60
- const total = toCount(countResult);
61
- const totalPages = Math.ceil(total / limit);
62
-
63
- let query = this.buildBaseQuery(state);
64
- query = this.applySelect(query, state);
65
- query = this.applySorts(query, state);
66
- query = query.limit(limit).offset(offset);
67
- const data = await query.execute();
68
-
69
- return {
70
- data: data as Record<string, unknown>[],
71
- meta: { total, page, limit, totalPages },
72
- };
73
- }
74
-
75
- async findOne(state: QueryState): Promise<Record<string, unknown> | null> {
76
- let query = this.buildBaseQuery(state);
77
- query = this.applySelect(query, state);
78
- query = this.applySorts(query, state);
79
- query = query.limit(1);
80
- const result = await query.executeTakeFirst();
81
- return (result as Record<string, unknown>) ?? null;
82
- }
83
-
84
- async count(state: QueryState): Promise<number> {
85
- let query = this.buildBaseQuery(state);
86
- query = query.select(sql`count(*)`.as('count'));
87
- const result = await query.executeTakeFirst();
88
- return toCount(result);
89
- }
90
-
91
- async get(id: string): Promise<Record<string, unknown> | null> {
92
- let query = this.db.selectFrom(this.tableName).selectAll().where('id', '=', id);
93
- if (this.model.traits.includes('soft_delete')) {
94
- query = query.where('archived_at', 'is', null);
95
- }
96
- const result = await query.executeTakeFirst();
97
- return (result as Record<string, unknown>) ?? null;
98
- }
99
-
100
- async create(
101
- data: Record<string, unknown>,
102
- _auth?: RequestContext,
103
- ): Promise<Record<string, unknown>> {
104
- await this.assignSequenceValues(data);
105
-
106
- const record = await this.db
107
- .insertInto(this.tableName)
108
- .values(data)
109
- .returningAll()
110
- .executeTakeFirstOrThrow();
111
-
112
- return record as Record<string, unknown>;
113
- }
114
-
115
- async update(
116
- id: string,
117
- data: Record<string, unknown>,
118
- _auth?: RequestContext,
119
- ): Promise<Record<string, unknown>> {
120
- await this.getOrThrow(id);
121
-
122
- const record = await this.db
123
- .updateTable(this.tableName)
124
- .set(data)
125
- .where('id', '=', id)
126
- .returningAll()
127
- .executeTakeFirstOrThrow();
128
-
129
- return record as Record<string, unknown>;
130
- }
131
-
132
- async delete(id: string, _auth?: RequestContext): Promise<Record<string, unknown>> {
133
- const record = await this.getOrThrow(id);
134
-
135
- await this.cascadeDeleteChildren(id);
136
-
137
- if (this.model.traits.includes('soft_delete')) {
138
- await this.db
139
- .updateTable(this.tableName)
140
- .set({ archived_at: new Date().toISOString() })
141
- .where('id', '=', id)
142
- .execute();
143
- } else {
144
- await this.db.deleteFrom(this.tableName).where('id', '=', id).execute();
145
- }
146
-
147
- return record;
148
- }
149
-
150
- withTransaction(trx: unknown): ModelOps {
151
- return new KyselyModelOps({
152
- db: trx as DbLike,
153
- model: this.model,
154
- registry: this.registry,
155
- auth: this.defaultAuth,
156
- tableName: modelToTableName(this.model.qualifiedName),
157
- });
158
- }
159
-
160
- compile(state: QueryState): unknown {
161
- let query = this.buildBaseQuery(state);
162
- query = this.applySelect(query, state);
163
- query = this.applySorts(query, state);
164
- query = this.applyPagination(query, state);
165
- return query.compile();
166
- }
167
-
168
- compileCount(state: QueryState): unknown {
169
- let query = this.buildBaseQuery(state);
170
- query = query.select(sql`count(*)`.as('count'));
171
- return query.compile();
172
- }
173
-
174
- private buildBaseQuery(state: QueryState) {
175
- let query = this.db.selectFrom(this.tableName);
176
-
177
- if (this.model.traits.includes('soft_delete') && !state.includeArchivedFlag) {
178
- query = query.where('archived_at', 'is', null);
179
- }
180
-
181
- query = applyModelFilters(query, state.filters);
182
-
183
- if (state.searchTerm && state.searchFields && state.searchFields.length > 0) {
184
- query = applySearchFilter(query, state.searchTerm, state.searchFields);
185
- }
186
-
187
- if (!state.unscopedFlag && state.auth) {
188
- query = applyScopeEnforcement(query, state.auth, {
189
- model: this.model,
190
- checkOwnership: true,
191
- });
192
- }
193
-
194
- return query;
195
- }
196
-
197
- private applySelect(query: any, state: QueryState) {
198
- return state.fieldNames.length > 0 ? query.select(state.fieldNames) : query.selectAll();
199
- }
200
-
201
- private applySorts(query: any, state: QueryState) {
202
- for (const { field, direction } of state.sorts) {
203
- query = query.orderBy(field, direction);
204
- }
205
- return query;
206
- }
207
-
208
- private applyPagination(query: any, state: QueryState) {
209
- if (state.limitVal !== undefined) {
210
- query = query.limit(state.limitVal);
211
- }
212
- if (state.offsetVal !== undefined) {
213
- query = query.offset(state.offsetVal);
214
- } else if (state.pageVal !== undefined) {
215
- const limit = state.limitVal ?? 25;
216
- const offset = (state.pageVal - 1) * limit;
217
- if (state.limitVal === undefined) query = query.limit(limit);
218
- query = query.offset(offset);
219
- }
220
- return query;
221
- }
222
-
223
- private async assignSequenceValues(data: Record<string, unknown>): Promise<void> {
224
- const sequenceFields = this.model.fields.filter(
225
- (f) => f.config.type === 'sequence' && !data[f.name],
226
- );
227
- if (sequenceFields.length === 0) return;
228
-
229
- for (const field of sequenceFields) {
230
- const config = field.config as { prefix?: string; digits?: number };
231
- const result = await (this.db as any)
232
- .insertInto('naming_sequence')
233
- .values({ model: this.model.qualifiedName, field: field.name, next_val: 1 })
234
- .onConflict((oc: any) =>
235
- oc
236
- .columns(['model', 'field'])
237
- .doUpdateSet({ next_val: sql`naming_sequence.next_val + 1` }),
238
- )
239
- .returning('next_val')
240
- .executeTakeFirstOrThrow();
241
-
242
- const num = String(result.next_val);
243
- const padded = config.digits ? num.padStart(config.digits, '0') : num;
244
- data[field.name] = (config.prefix ?? '') + padded;
245
- }
246
- }
247
-
248
- private async cascadeDeleteChildren(parentId: string): Promise<void> {
249
- const childrenFields = this.model.fields.filter((f) => f.config.type === 'children');
250
- if (childrenFields.length === 0) return;
251
-
252
- for (const field of childrenFields) {
253
- const config = field.config as { model: string; foreignKey: string };
254
- const qualifiedChild = config.model.includes('.')
255
- ? config.model
256
- : `${this.model.module}.${config.model}`;
257
- const childTable = modelToTableName(qualifiedChild);
258
- const childModel = this.registry.getModel(qualifiedChild);
259
- const isSoftDelete = childModel?.traits.includes('soft_delete');
260
-
261
- if (isSoftDelete) {
262
- await (this.db as any)
263
- .updateTable(childTable)
264
- .set({ archived_at: new Date().toISOString() })
265
- .where(config.foreignKey, '=', parentId)
266
- .execute();
267
- } else {
268
- await (this.db as any)
269
- .deleteFrom(childTable)
270
- .where(config.foreignKey, '=', parentId)
271
- .execute();
272
- }
273
- }
274
- }
275
-
276
- private async getOrThrow(id: string): Promise<Record<string, unknown>> {
277
- const record = await this.get(id);
278
- if (!record) throw new NotFoundError(`Record not found: ${id}`);
279
- return record;
280
- }
281
- }
@@ -1,37 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import type { ResolvedModel } from '../schema/types.js';
3
- import type { RequestContext } from '../auth/types.js';
4
-
5
- export interface ScopeEnforcementOptions {
6
- model?: ResolvedModel;
7
- checkOwnership?: boolean;
8
- }
9
-
10
- export function applyScopeEnforcement(
11
- query: any,
12
- auth: RequestContext | undefined,
13
- options?: ScopeEnforcementOptions,
14
- ): any {
15
- if (!auth) return query;
16
-
17
- let result = query;
18
-
19
- if (auth.scopeFilters && auth.scopeFilters.length > 0) {
20
- for (const filter of auth.scopeFilters) {
21
- const operator = filter.operator === 'in' ? 'in' : '=';
22
- result = result.where(filter.field, operator, filter.value);
23
- }
24
- }
25
-
26
- if (options?.checkOwnership && options.model && auth.permissions && auth.user) {
27
- const modelPerms = auth.permissions.models[options.model.qualifiedName];
28
- if (modelPerms && modelPerms.read === 'own') {
29
- const hasCreatedBy = options.model.fields.some((f) => f.name === 'created_by');
30
- if (hasCreatedBy) {
31
- result = result.where('created_by', '=', auth.user.id);
32
- }
33
- }
34
- }
35
-
36
- return result;
37
- }
package/src/db/types.ts DELETED
@@ -1,98 +0,0 @@
1
- export interface ColumnDefinition {
2
- name: string;
3
- type: string;
4
- nullable: boolean;
5
- defaultValue?: string;
6
- primaryKey?: boolean;
7
- }
8
-
9
- export interface ForeignKeyDefinition {
10
- name: string;
11
- column: string;
12
- referencedTable: string;
13
- referencedColumn: string;
14
- }
15
-
16
- export interface CheckConstraintDefinition {
17
- name: string;
18
- column: string;
19
- expression: string;
20
- }
21
-
22
- export interface IndexDefinition {
23
- name: string;
24
- table: string;
25
- columns: string[];
26
- unique: boolean;
27
- where?: string;
28
- }
29
-
30
- export interface TableDefinition {
31
- name: string;
32
- columns: ColumnDefinition[];
33
- foreignKeys: ForeignKeyDefinition[];
34
- checkConstraints: CheckConstraintDefinition[];
35
- indexes: IndexDefinition[];
36
- }
37
-
38
- export interface DesiredState {
39
- tables: TableDefinition[];
40
- }
41
-
42
- export interface ActualColumn {
43
- name: string;
44
- type: string;
45
- nullable: boolean;
46
- defaultValue: string | null;
47
- }
48
-
49
- export interface ActualForeignKey {
50
- name: string;
51
- column: string;
52
- referencedTable: string;
53
- referencedColumn: string;
54
- }
55
-
56
- export interface ActualIndex {
57
- name: string;
58
- columns: string[];
59
- unique: boolean;
60
- }
61
-
62
- export interface ActualCheckConstraint {
63
- name: string;
64
- column: string;
65
- expression: string;
66
- }
67
-
68
- export interface ActualTable {
69
- name: string;
70
- columns: ActualColumn[];
71
- foreignKeys: ActualForeignKey[];
72
- indexes: ActualIndex[];
73
- checkConstraints: ActualCheckConstraint[];
74
- }
75
-
76
- export interface ActualState {
77
- tables: ActualTable[];
78
- }
79
-
80
- export type DdlOperationType =
81
- | 'CREATE_TABLE'
82
- | 'ADD_COLUMN'
83
- | 'ALTER_COLUMN_TYPE'
84
- | 'ADD_FOREIGN_KEY'
85
- | 'ADD_CHECK_CONSTRAINT'
86
- | 'CREATE_INDEX'
87
- | 'DROP_COLUMN'
88
- | 'DROP_TABLE'
89
- | 'DROP_FOREIGN_KEY'
90
- | 'DROP_INDEX';
91
-
92
- export interface DdlOperation {
93
- type: DdlOperationType;
94
- table: string;
95
- sql: string;
96
- destructive: boolean;
97
- detail?: string;
98
- }
package/src/errors.ts DELETED
@@ -1,41 +0,0 @@
1
- export class AppError extends Error {
2
- public readonly statusCode: number;
3
- public readonly code: string;
4
- public readonly details?: unknown;
5
-
6
- constructor(statusCode: number, code: string, message: string, details?: unknown) {
7
- super(message);
8
- this.name = 'AppError';
9
- this.statusCode = statusCode;
10
- this.code = code;
11
- this.details = details;
12
- }
13
- }
14
-
15
- export class BadRequestError extends AppError {
16
- constructor(code: string, message: string, details?: unknown) {
17
- super(400, code, message, details);
18
- this.name = 'BadRequestError';
19
- }
20
- }
21
-
22
- export class UnauthorizedError extends AppError {
23
- constructor(message: string) {
24
- super(401, 'UNAUTHORIZED', message);
25
- this.name = 'UnauthorizedError';
26
- }
27
- }
28
-
29
- export class ForbiddenError extends AppError {
30
- constructor(code: string, message: string, details?: unknown) {
31
- super(403, code, message, details);
32
- this.name = 'ForbiddenError';
33
- }
34
- }
35
-
36
- export class NotFoundError extends AppError {
37
- constructor(message: string) {
38
- super(404, 'NOT_FOUND', message);
39
- this.name = 'NotFoundError';
40
- }
41
- }
@@ -1,105 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { EventBus } from '../bus.js';
3
-
4
- describe('EventBus', () => {
5
- it('registers listeners via on()', () => {
6
- const bus = new EventBus();
7
- const handler = vi.fn();
8
-
9
- bus.on('order.created', handler);
10
-
11
- expect(bus.hasListeners('order.created')).toBe(true);
12
- expect(bus.getListeners('order.created')).toHaveLength(1);
13
- });
14
-
15
- it('supports multiple listeners per event', () => {
16
- const bus = new EventBus();
17
- const h1 = vi.fn();
18
- const h2 = vi.fn();
19
-
20
- bus.on('order.created', h1);
21
- bus.on('order.created', h2);
22
-
23
- expect(bus.getListeners('order.created')).toHaveLength(2);
24
- });
25
-
26
- it('returns false for events with no listeners', () => {
27
- const bus = new EventBus();
28
- expect(bus.hasListeners('nothing')).toBe(false);
29
- expect(bus.getListeners('nothing')).toHaveLength(0);
30
- });
31
-
32
- it('getAllEvents returns all registered event names', () => {
33
- const bus = new EventBus();
34
- bus.on('a', vi.fn());
35
- bus.on('b', vi.fn());
36
- bus.on('c', vi.fn());
37
-
38
- const events = bus.getAllEvents();
39
- expect(events).toContain('a');
40
- expect(events).toContain('b');
41
- expect(events).toContain('c');
42
- });
43
-
44
- describe('sync emit', () => {
45
- it('calls listeners inline with { sync: true }', async () => {
46
- const bus = new EventBus();
47
- const handler = vi.fn().mockResolvedValue(undefined);
48
-
49
- bus.on('test.event', handler);
50
- await bus.emit('test.event', { id: 1 }, { sync: true });
51
-
52
- expect(handler).toHaveBeenCalledWith({ id: 1 });
53
- });
54
-
55
- it('calls multiple listeners in order', async () => {
56
- const bus = new EventBus();
57
- const order: number[] = [];
58
-
59
- bus.on('test', async () => {
60
- order.push(1);
61
- });
62
- bus.on('test', async () => {
63
- order.push(2);
64
- });
65
-
66
- await bus.emit('test', {}, { sync: true });
67
- expect(order).toEqual([1, 2]);
68
- });
69
-
70
- it('does nothing if no listeners registered', async () => {
71
- const bus = new EventBus();
72
- await expect(bus.emit('unknown', {}, { sync: true })).resolves.toBeUndefined();
73
- });
74
- });
75
-
76
- describe('async emit', () => {
77
- it('throws if db not set', async () => {
78
- const bus = new EventBus();
79
- bus.on('test', vi.fn());
80
-
81
- await expect(bus.emit('test', {})).rejects.toThrow('database not set');
82
- });
83
-
84
- it('emitWithTrx in sync mode calls handlers directly', async () => {
85
- const bus = new EventBus();
86
- const handler = vi.fn().mockResolvedValue(undefined);
87
- bus.on('test', handler);
88
-
89
- const mockTrx = {} as any;
90
- await bus.emitWithTrx('test', { data: 'hi' }, mockTrx, { sync: true });
91
-
92
- expect(handler).toHaveBeenCalledWith({ data: 'hi' });
93
- });
94
- });
95
-
96
- describe('listener source tracking', () => {
97
- it('stores source on listener', () => {
98
- const bus = new EventBus();
99
- bus.on('test', vi.fn(), 'sales-module');
100
-
101
- const listeners = bus.getListeners('test');
102
- expect(listeners[0].source).toBe('sales-module');
103
- });
104
- });
105
- });
package/src/events/bus.ts DELETED
@@ -1,89 +0,0 @@
1
- import type { Kysely } from 'kysely';
2
- import { enqueue } from '../jobs/enqueue.js';
3
- import type { EventListener } from './types.js';
4
-
5
- /**
6
- * Simple pub/sub event bus that supports two dispatch modes:
7
- * - sync: invoke listeners directly in sequence
8
- * - async (default): enqueue the event as a background job via the database
9
- */
10
- export class EventBus {
11
- private readonly listeners: Map<string, EventListener[]> = new Map();
12
- private db: Kysely<unknown> | null = null;
13
-
14
- /** Provide the database connection used for async event dispatch. */
15
- setDb(db: Kysely<unknown>): void {
16
- this.db = db;
17
- }
18
-
19
- /** Register a listener for a specific event. */
20
- on(event: string, handler: (payload: unknown) => Promise<void>, source?: string): void {
21
- if (!this.listeners.has(event)) {
22
- this.listeners.set(event, []);
23
- }
24
- this.listeners.get(event)!.push({ event, handler, source });
25
- }
26
-
27
- /**
28
- * Emit an event. By default, the event is enqueued as a background job.
29
- * Pass { sync: true } to invoke listeners immediately in sequence.
30
- */
31
- async emit(event: string, payload: unknown, options?: { sync?: boolean }): Promise<void> {
32
- if (options?.sync) {
33
- await this.dispatchSync(event, payload);
34
- } else {
35
- await this.dispatchAsync(event, payload);
36
- }
37
- }
38
-
39
- /**
40
- * Emit an event within an existing database transaction.
41
- * Sync mode invokes listeners directly; async mode enqueues via the transaction.
42
- */
43
- async emitWithTrx(
44
- event: string,
45
- payload: unknown,
46
- trx: Kysely<unknown>,
47
- options?: { sync?: boolean },
48
- ): Promise<void> {
49
- if (options?.sync) {
50
- await this.dispatchSync(event, payload);
51
- } else {
52
- await enqueue(trx, `__event:${event}`, payload);
53
- }
54
- }
55
-
56
- /** Return all listeners registered for a given event. */
57
- getListeners(event: string): EventListener[] {
58
- return this.listeners.get(event) ?? [];
59
- }
60
-
61
- /** Return the names of all events that have at least one listener. */
62
- getAllEvents(): string[] {
63
- return Array.from(this.listeners.keys());
64
- }
65
-
66
- /** Check whether any listeners are registered for a given event. */
67
- hasListeners(event: string): boolean {
68
- const eventListeners = this.listeners.get(event);
69
- return !!eventListeners && eventListeners.length > 0;
70
- }
71
-
72
- // --- Private dispatch methods ---
73
-
74
- /** Invoke all listeners for an event synchronously, one after another. */
75
- private async dispatchSync(event: string, payload: unknown): Promise<void> {
76
- const eventListeners = this.listeners.get(event) ?? [];
77
- for (const listener of eventListeners) {
78
- await listener.handler(payload);
79
- }
80
- }
81
-
82
- /** Enqueue the event as a background job via the database. */
83
- private async dispatchAsync(event: string, payload: unknown): Promise<void> {
84
- if (!this.db) {
85
- throw new Error('EventBus: database not set. Call setDb() before emitting async events.');
86
- }
87
- await enqueue(this.db, `__event:${event}`, payload);
88
- }
89
- }
@@ -1,2 +0,0 @@
1
- export type { EventListener, EventBusConfig } from './types.js';
2
- export { EventBus } from './bus.js';
@@ -1,9 +0,0 @@
1
- export interface EventListener {
2
- event: string;
3
- handler: (payload: unknown) => Promise<void>;
4
- source?: string;
5
- }
6
-
7
- export interface EventBusConfig {
8
- sync?: boolean;
9
- }