@fragno-dev/db 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +48 -41
- package/CHANGELOG.md +6 -0
- package/dist/adapters/adapters.d.ts +13 -1
- package/dist/adapters/adapters.d.ts.map +1 -1
- package/dist/adapters/adapters.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.d.ts +2 -0
- package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-adapter.js +6 -1
- package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-query.js +6 -4
- package/dist/adapters/drizzle/drizzle-query.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-compiler.js +49 -36
- package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js +1 -1
- package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +1 -1
- package/dist/adapters/drizzle/shared.d.ts +14 -1
- package/dist/adapters/drizzle/shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts +2 -0
- package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
- package/dist/adapters/kysely/kysely-adapter.js +7 -2
- package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
- package/dist/adapters/kysely/kysely-query.js +5 -3
- package/dist/adapters/kysely/kysely-query.js.map +1 -1
- package/dist/adapters/kysely/kysely-shared.d.ts +11 -0
- package/dist/adapters/kysely/kysely-shared.d.ts.map +1 -0
- package/dist/adapters/kysely/kysely-uow-compiler.js +38 -9
- package/dist/adapters/kysely/kysely-uow-compiler.js.map +1 -1
- package/dist/bind-services.d.ts +7 -0
- package/dist/bind-services.d.ts.map +1 -0
- package/dist/bind-services.js +14 -0
- package/dist/bind-services.js.map +1 -0
- package/dist/fragment.d.ts +131 -12
- package/dist/fragment.d.ts.map +1 -1
- package/dist/fragment.js +107 -8
- package/dist/fragment.js.map +1 -1
- package/dist/mod.d.ts +4 -2
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +3 -2
- package/dist/mod.js.map +1 -1
- package/dist/query/query.d.ts +2 -2
- package/dist/query/query.d.ts.map +1 -1
- package/dist/query/unit-of-work.d.ts +100 -15
- package/dist/query/unit-of-work.d.ts.map +1 -1
- package/dist/query/unit-of-work.js +214 -7
- package/dist/query/unit-of-work.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/adapters.ts +14 -0
- package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +6 -1
- package/src/adapters/drizzle/drizzle-adapter-sqlite.test.ts +133 -5
- package/src/adapters/drizzle/drizzle-adapter.ts +16 -1
- package/src/adapters/drizzle/drizzle-query.ts +26 -15
- package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +57 -57
- package/src/adapters/drizzle/drizzle-uow-compiler.ts +79 -39
- package/src/adapters/drizzle/drizzle-uow-decoder.ts +2 -5
- package/src/adapters/kysely/kysely-adapter-pglite.test.ts +2 -2
- package/src/adapters/kysely/kysely-adapter.ts +16 -1
- package/src/adapters/kysely/kysely-query.ts +26 -15
- package/src/adapters/kysely/kysely-uow-compiler.test.ts +43 -43
- package/src/adapters/kysely/kysely-uow-compiler.ts +50 -14
- package/src/adapters/kysely/kysely-uow-joins.test.ts +30 -30
- package/src/bind-services.test.ts +214 -0
- package/src/bind-services.ts +37 -0
- package/src/db-fragment.test.ts +800 -0
- package/src/fragment.ts +557 -28
- package/src/mod.ts +19 -0
- package/src/query/query.ts +2 -2
- package/src/query/unit-of-work-multi-schema.test.ts +64 -0
- package/src/query/unit-of-work-types.test.ts +13 -0
- package/src/query/unit-of-work.test.ts +5 -9
- package/src/query/unit-of-work.ts +511 -62
- package/src/uow-context-integration.test.ts +102 -0
- package/src/uow-context.test.ts +182 -0
- package/src/fragment.test.ts +0 -341
package/src/fragment.ts
CHANGED
|
@@ -1,7 +1,79 @@
|
|
|
1
1
|
import type { AnySchema } from "./schema/create";
|
|
2
2
|
import type { AbstractQuery } from "./query/query";
|
|
3
3
|
import type { DatabaseAdapter } from "./adapters/adapters";
|
|
4
|
-
import
|
|
4
|
+
import { bindServicesToContext, type BoundServices } from "./bind-services";
|
|
5
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
6
|
+
import type { IUnitOfWorkBase, UnitOfWorkSchemaView } from "./query/unit-of-work";
|
|
7
|
+
import type { RequestThisContext } from "@fragno-dev/core/api";
|
|
8
|
+
|
|
9
|
+
export const uowStorage = new AsyncLocalStorage<IUnitOfWorkBase>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Service context for database fragments, providing access to the Unit of Work.
|
|
13
|
+
*/
|
|
14
|
+
export interface DatabaseRequestThisContext extends RequestThisContext {
|
|
15
|
+
/**
|
|
16
|
+
* Get the Unit of Work from the current context.
|
|
17
|
+
* @param schema - Optional schema to get a typed view. If not provided, returns the base UOW.
|
|
18
|
+
* @returns IUnitOfWorkBase if no schema provided, or typed UnitOfWorkSchemaView if schema provided.
|
|
19
|
+
*/
|
|
20
|
+
getUnitOfWork(): IUnitOfWorkBase;
|
|
21
|
+
getUnitOfWork<TSchema extends AnySchema>(
|
|
22
|
+
schema: TSchema,
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
): UnitOfWorkSchemaView<TSchema, [], any>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const serviceContext: DatabaseRequestThisContext = {
|
|
28
|
+
getUnitOfWork<TSchema extends AnySchema>(
|
|
29
|
+
schema?: TSchema,
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
): any {
|
|
32
|
+
const uow = uowStorage.getStore();
|
|
33
|
+
if (!uow) {
|
|
34
|
+
throw new Error("No UnitOfWork in context. Service must be called within a route handler.");
|
|
35
|
+
}
|
|
36
|
+
if (schema) {
|
|
37
|
+
return uow.forSchema(schema);
|
|
38
|
+
}
|
|
39
|
+
return uow;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function withUnitOfWork<T>(uow: IUnitOfWorkBase, callback: () => T): Promise<T> {
|
|
44
|
+
return Promise.resolve(uowStorage.run(uow, callback));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Type helper that enforces DatabaseRequestThisContext on all functions in a service object
|
|
49
|
+
*/
|
|
50
|
+
type WithDatabaseThis<T> = {
|
|
51
|
+
[K in keyof T]: T[K] extends (...args: infer A) => infer R
|
|
52
|
+
? (this: DatabaseRequestThisContext, ...args: A) => R
|
|
53
|
+
: T[K] extends Record<string, unknown>
|
|
54
|
+
? WithDatabaseThis<T[K]>
|
|
55
|
+
: T[K];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Import types from fragno package
|
|
59
|
+
import type {
|
|
60
|
+
FragmentDefinition,
|
|
61
|
+
RouteHandler,
|
|
62
|
+
FragnoPublicConfig,
|
|
63
|
+
RequestInputContext,
|
|
64
|
+
RequestOutputContext,
|
|
65
|
+
} from "@fragno-dev/core";
|
|
66
|
+
|
|
67
|
+
export { bindServicesToContext, type BoundServices };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Route handler type for database fragments with access to Unit of Work.
|
|
71
|
+
*/
|
|
72
|
+
export type DatabaseRouteHandler = (
|
|
73
|
+
this: DatabaseRequestThisContext,
|
|
74
|
+
inputContext: RequestInputContext,
|
|
75
|
+
outputContext: RequestOutputContext,
|
|
76
|
+
) => Promise<Response>;
|
|
5
77
|
|
|
6
78
|
/**
|
|
7
79
|
* Extended FragnoPublicConfig that includes a database adapter.
|
|
@@ -25,8 +97,20 @@ export class DatabaseFragmentBuilder<
|
|
|
25
97
|
const TSchema extends AnySchema,
|
|
26
98
|
const TConfig,
|
|
27
99
|
const TDeps = {},
|
|
28
|
-
const TServices
|
|
100
|
+
const TServices = {},
|
|
101
|
+
const TUsedServices = {},
|
|
102
|
+
const TProvidedServices = {},
|
|
29
103
|
> {
|
|
104
|
+
// Type-only property to expose type parameters for better inference
|
|
105
|
+
readonly $types!: {
|
|
106
|
+
schema: TSchema;
|
|
107
|
+
config: TConfig;
|
|
108
|
+
deps: TDeps;
|
|
109
|
+
services: TServices;
|
|
110
|
+
usedServices: TUsedServices;
|
|
111
|
+
providedServices: TProvidedServices;
|
|
112
|
+
};
|
|
113
|
+
|
|
30
114
|
#name: string;
|
|
31
115
|
#schema?: TSchema;
|
|
32
116
|
#namespace?: string;
|
|
@@ -40,9 +124,14 @@ export class DatabaseFragmentBuilder<
|
|
|
40
124
|
context: {
|
|
41
125
|
config: TConfig;
|
|
42
126
|
fragnoConfig: FragnoPublicConfig;
|
|
43
|
-
deps: TDeps;
|
|
127
|
+
deps: TDeps & TUsedServices;
|
|
128
|
+
defineService: <T extends Record<string, unknown>>(
|
|
129
|
+
services: WithDatabaseThis<T>,
|
|
130
|
+
) => WithDatabaseThis<T>;
|
|
44
131
|
} & DatabaseFragmentContext<TSchema>,
|
|
45
132
|
) => TServices;
|
|
133
|
+
#usedServices?: Record<string, { name: string; required: boolean }>;
|
|
134
|
+
#providedServices?: Record<string, unknown>;
|
|
46
135
|
|
|
47
136
|
constructor(options: {
|
|
48
137
|
name: string;
|
|
@@ -58,27 +147,43 @@ export class DatabaseFragmentBuilder<
|
|
|
58
147
|
context: {
|
|
59
148
|
config: TConfig;
|
|
60
149
|
fragnoConfig: FragnoPublicConfig;
|
|
61
|
-
deps: TDeps;
|
|
150
|
+
deps: TDeps & TUsedServices;
|
|
151
|
+
defineService: <T extends Record<string, unknown>>(
|
|
152
|
+
services: WithDatabaseThis<T>,
|
|
153
|
+
) => WithDatabaseThis<T>;
|
|
62
154
|
} & DatabaseFragmentContext<TSchema>,
|
|
63
155
|
) => TServices;
|
|
156
|
+
usedServices?: Record<string, { name: string; required: boolean }>;
|
|
157
|
+
providedServices?: Record<string, unknown>;
|
|
64
158
|
}) {
|
|
65
159
|
this.#name = options.name;
|
|
66
160
|
this.#schema = options.schema;
|
|
67
161
|
this.#namespace = options.namespace;
|
|
68
162
|
this.#dependencies = options.dependencies;
|
|
69
163
|
this.#services = options.services;
|
|
164
|
+
this.#usedServices = options.usedServices;
|
|
165
|
+
this.#providedServices = options.providedServices;
|
|
70
166
|
}
|
|
71
167
|
|
|
72
168
|
get $requiredOptions(): FragnoPublicConfigWithDatabase {
|
|
73
169
|
throw new Error("Type only method. Do not call.");
|
|
74
170
|
}
|
|
75
171
|
|
|
76
|
-
get definition(): FragmentDefinition<
|
|
172
|
+
get definition(): FragmentDefinition<
|
|
173
|
+
TConfig,
|
|
174
|
+
TDeps,
|
|
175
|
+
BoundServices<TServices>,
|
|
176
|
+
{ databaseSchema?: TSchema; databaseNamespace: string },
|
|
177
|
+
BoundServices<TUsedServices>,
|
|
178
|
+
BoundServices<TProvidedServices>,
|
|
179
|
+
DatabaseRequestThisContext
|
|
180
|
+
> {
|
|
77
181
|
const schema = this.#schema;
|
|
78
182
|
const namespace = this.#namespace ?? "";
|
|
79
183
|
const name = this.#name;
|
|
80
184
|
const dependencies = this.#dependencies;
|
|
81
185
|
const services = this.#services;
|
|
186
|
+
const providedServices = this.#providedServices;
|
|
82
187
|
|
|
83
188
|
return {
|
|
84
189
|
name,
|
|
@@ -86,16 +191,80 @@ export class DatabaseFragmentBuilder<
|
|
|
86
191
|
const dbContext = this.#createDatabaseContext(options, schema, namespace, name);
|
|
87
192
|
return dependencies?.({ config, fragnoConfig: options, ...dbContext }) ?? ({} as TDeps);
|
|
88
193
|
},
|
|
89
|
-
services: (
|
|
194
|
+
services: (
|
|
195
|
+
config: TConfig,
|
|
196
|
+
options: FragnoPublicConfig,
|
|
197
|
+
deps: TDeps & BoundServices<TUsedServices>,
|
|
198
|
+
) => {
|
|
90
199
|
const dbContext = this.#createDatabaseContext(options, schema, namespace, name);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
200
|
+
// Cast deps back to raw type for internal services function.
|
|
201
|
+
// This is safe because:
|
|
202
|
+
// 1. deps are already bound (their 'this' parameters are stripped)
|
|
203
|
+
// 2. The services function expects raw types but only uses the public API
|
|
204
|
+
// 3. BoundServices<T> has the same runtime shape as T (just without 'this')
|
|
205
|
+
|
|
206
|
+
// defineService provides typing for service functions
|
|
207
|
+
// It expects the input to already have proper 'this' types on functions
|
|
208
|
+
const defineService = <T extends Record<string, unknown>>(
|
|
209
|
+
services: WithDatabaseThis<T>,
|
|
210
|
+
): WithDatabaseThis<T> => services;
|
|
211
|
+
|
|
212
|
+
const rawServices =
|
|
213
|
+
services?.({
|
|
214
|
+
config,
|
|
215
|
+
fragnoConfig: options,
|
|
216
|
+
deps: deps as TDeps & TUsedServices,
|
|
217
|
+
defineService,
|
|
218
|
+
...dbContext,
|
|
219
|
+
}) ?? ({} as TServices);
|
|
220
|
+
|
|
221
|
+
// Bind all service methods to serviceContext
|
|
222
|
+
return bindServicesToContext(
|
|
223
|
+
rawServices as Record<string, unknown>,
|
|
224
|
+
) as BoundServices<TServices>;
|
|
94
225
|
},
|
|
95
226
|
additionalContext: {
|
|
96
227
|
databaseSchema: schema,
|
|
97
228
|
databaseNamespace: namespace,
|
|
98
229
|
},
|
|
230
|
+
createHandlerWrapper: schema
|
|
231
|
+
? (options: FragnoPublicConfig) => {
|
|
232
|
+
const dbContext = this.#createDatabaseContext(options, schema, namespace, name);
|
|
233
|
+
const { orm } = dbContext;
|
|
234
|
+
|
|
235
|
+
// Return handler wrapper function
|
|
236
|
+
return (handler: DatabaseRouteHandler): RouteHandler => {
|
|
237
|
+
return async (inputContext, outputContext) => {
|
|
238
|
+
// Create UOW for this request
|
|
239
|
+
const uow = orm.createUnitOfWork();
|
|
240
|
+
|
|
241
|
+
// Execute handler within AsyncLocalStorage context
|
|
242
|
+
return withUnitOfWork(uow, async () => {
|
|
243
|
+
// Bind handler to serviceContext so it has access to getUnitOfWork via 'this'
|
|
244
|
+
const boundHandler = handler.bind(serviceContext);
|
|
245
|
+
return boundHandler(inputContext, outputContext);
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
: undefined,
|
|
251
|
+
usedServices: this.#usedServices as
|
|
252
|
+
| {
|
|
253
|
+
[K in keyof TUsedServices]: { name: string; required: boolean };
|
|
254
|
+
}
|
|
255
|
+
| undefined,
|
|
256
|
+
// Pass providedServices as-is - let fragment-instantiation.ts handle resolution
|
|
257
|
+
// The factory functions will be called by createFragment
|
|
258
|
+
providedServices: providedServices as
|
|
259
|
+
| {
|
|
260
|
+
[K in keyof BoundServices<TProvidedServices>]: BoundServices<TProvidedServices>[K];
|
|
261
|
+
}
|
|
262
|
+
| ((
|
|
263
|
+
config: TConfig,
|
|
264
|
+
options: FragnoPublicConfig,
|
|
265
|
+
deps: TDeps & BoundServices<TUsedServices>,
|
|
266
|
+
) => BoundServices<TProvidedServices>)
|
|
267
|
+
| undefined,
|
|
99
268
|
};
|
|
100
269
|
}
|
|
101
270
|
|
|
@@ -128,8 +297,22 @@ export class DatabaseFragmentBuilder<
|
|
|
128
297
|
withDatabase<TNewSchema extends AnySchema>(
|
|
129
298
|
schema: TNewSchema,
|
|
130
299
|
namespace?: string,
|
|
131
|
-
): DatabaseFragmentBuilder<
|
|
132
|
-
|
|
300
|
+
): DatabaseFragmentBuilder<
|
|
301
|
+
TNewSchema,
|
|
302
|
+
TConfig,
|
|
303
|
+
TDeps,
|
|
304
|
+
TServices,
|
|
305
|
+
TUsedServices,
|
|
306
|
+
TProvidedServices
|
|
307
|
+
> {
|
|
308
|
+
return new DatabaseFragmentBuilder<
|
|
309
|
+
TNewSchema,
|
|
310
|
+
TConfig,
|
|
311
|
+
TDeps,
|
|
312
|
+
TServices,
|
|
313
|
+
TUsedServices,
|
|
314
|
+
TProvidedServices
|
|
315
|
+
>({
|
|
133
316
|
name: this.#name,
|
|
134
317
|
schema,
|
|
135
318
|
namespace: namespace ?? this.#name + "-db",
|
|
@@ -146,10 +329,15 @@ export class DatabaseFragmentBuilder<
|
|
|
146
329
|
context: {
|
|
147
330
|
config: TConfig;
|
|
148
331
|
fragnoConfig: FragnoPublicConfig;
|
|
149
|
-
deps: TDeps;
|
|
332
|
+
deps: TDeps & TUsedServices;
|
|
333
|
+
defineService: <T extends Record<string, unknown>>(
|
|
334
|
+
services: WithDatabaseThis<T>,
|
|
335
|
+
) => WithDatabaseThis<T>;
|
|
150
336
|
} & DatabaseFragmentContext<TNewSchema>,
|
|
151
337
|
) => TServices)
|
|
152
338
|
| undefined,
|
|
339
|
+
usedServices: this.#usedServices,
|
|
340
|
+
providedServices: this.#providedServices,
|
|
153
341
|
});
|
|
154
342
|
}
|
|
155
343
|
|
|
@@ -160,39 +348,380 @@ export class DatabaseFragmentBuilder<
|
|
|
160
348
|
fragnoConfig: FragnoPublicConfig;
|
|
161
349
|
} & DatabaseFragmentContext<TSchema>,
|
|
162
350
|
) => TNewDeps,
|
|
163
|
-
): DatabaseFragmentBuilder<TSchema, TConfig, TNewDeps, {}> {
|
|
164
|
-
return new DatabaseFragmentBuilder<
|
|
351
|
+
): DatabaseFragmentBuilder<TSchema, TConfig, TNewDeps, {}, TUsedServices, TProvidedServices> {
|
|
352
|
+
return new DatabaseFragmentBuilder<
|
|
353
|
+
TSchema,
|
|
354
|
+
TConfig,
|
|
355
|
+
TNewDeps,
|
|
356
|
+
{},
|
|
357
|
+
TUsedServices,
|
|
358
|
+
TProvidedServices
|
|
359
|
+
>({
|
|
165
360
|
name: this.#name,
|
|
166
361
|
schema: this.#schema,
|
|
167
362
|
namespace: this.#namespace,
|
|
168
363
|
dependencies: fn,
|
|
169
364
|
services: undefined,
|
|
365
|
+
usedServices: this.#usedServices,
|
|
366
|
+
providedServices: this.#providedServices,
|
|
170
367
|
});
|
|
171
368
|
}
|
|
172
369
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
): DatabaseFragmentBuilder<
|
|
182
|
-
|
|
370
|
+
/**
|
|
371
|
+
* Declare that this fragment uses a service.
|
|
372
|
+
* @param serviceName - The name of the service to use
|
|
373
|
+
* @param options - Optional configuration: { optional: boolean } (defaults to required)
|
|
374
|
+
*/
|
|
375
|
+
usesService<TServiceName extends string, TService>(
|
|
376
|
+
serviceName: TServiceName,
|
|
377
|
+
options?: { optional?: false },
|
|
378
|
+
): DatabaseFragmentBuilder<
|
|
379
|
+
TSchema,
|
|
380
|
+
TConfig,
|
|
381
|
+
TDeps,
|
|
382
|
+
TServices,
|
|
383
|
+
TUsedServices & { [K in TServiceName]: TService },
|
|
384
|
+
TProvidedServices
|
|
385
|
+
>;
|
|
386
|
+
usesService<TServiceName extends string, TService>(
|
|
387
|
+
serviceName: TServiceName,
|
|
388
|
+
options: { optional: true },
|
|
389
|
+
): DatabaseFragmentBuilder<
|
|
390
|
+
TSchema,
|
|
391
|
+
TConfig,
|
|
392
|
+
TDeps,
|
|
393
|
+
TServices,
|
|
394
|
+
TUsedServices & { [K in TServiceName]: TService | undefined },
|
|
395
|
+
TProvidedServices
|
|
396
|
+
>;
|
|
397
|
+
usesService<TServiceName extends string, TService>(
|
|
398
|
+
serviceName: TServiceName,
|
|
399
|
+
options?: { optional?: boolean },
|
|
400
|
+
): DatabaseFragmentBuilder<
|
|
401
|
+
TSchema,
|
|
402
|
+
TConfig,
|
|
403
|
+
TDeps,
|
|
404
|
+
TServices,
|
|
405
|
+
TUsedServices & { [K in TServiceName]: TService | TService | undefined },
|
|
406
|
+
TProvidedServices
|
|
407
|
+
> {
|
|
408
|
+
const isOptional = options?.optional ?? false;
|
|
409
|
+
return new DatabaseFragmentBuilder<
|
|
410
|
+
TSchema,
|
|
411
|
+
TConfig,
|
|
412
|
+
TDeps,
|
|
413
|
+
TServices,
|
|
414
|
+
TUsedServices & { [K in TServiceName]: TService | (TService | undefined) },
|
|
415
|
+
TProvidedServices
|
|
416
|
+
>({
|
|
183
417
|
name: this.#name,
|
|
184
418
|
schema: this.#schema,
|
|
185
419
|
namespace: this.#namespace,
|
|
186
|
-
dependencies: this.#dependencies
|
|
187
|
-
|
|
420
|
+
dependencies: this.#dependencies as unknown as
|
|
421
|
+
| ((
|
|
422
|
+
context: {
|
|
423
|
+
config: TConfig;
|
|
424
|
+
fragnoConfig: FragnoPublicConfig;
|
|
425
|
+
} & DatabaseFragmentContext<TSchema>,
|
|
426
|
+
) => TDeps)
|
|
427
|
+
| undefined,
|
|
428
|
+
services: this.#services as unknown as
|
|
429
|
+
| ((
|
|
430
|
+
context: {
|
|
431
|
+
config: TConfig;
|
|
432
|
+
fragnoConfig: FragnoPublicConfig;
|
|
433
|
+
deps: TDeps &
|
|
434
|
+
(TUsedServices & { [K in TServiceName]: TService | (TService | undefined) });
|
|
435
|
+
} & DatabaseFragmentContext<TSchema>,
|
|
436
|
+
) => TServices)
|
|
437
|
+
| undefined,
|
|
438
|
+
usedServices: {
|
|
439
|
+
...this.#usedServices,
|
|
440
|
+
[serviceName]: { name: serviceName, required: !isOptional },
|
|
441
|
+
},
|
|
442
|
+
providedServices: this.#providedServices,
|
|
188
443
|
});
|
|
189
444
|
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Define services for this fragment (unnamed).
|
|
448
|
+
* Functions in the service will have access to DatabaseRequestThisContext via `this` if using `defineService`.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* With `this` context:
|
|
452
|
+
* ```ts
|
|
453
|
+
* .providesService(({ defineService }) => defineService({
|
|
454
|
+
* createUser: function(name: string) {
|
|
455
|
+
* const uow = this.getUnitOfWork(mySchema);
|
|
456
|
+
* return uow.create('user', { name });
|
|
457
|
+
* }
|
|
458
|
+
* }))
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* Without `this` context:
|
|
462
|
+
* ```ts
|
|
463
|
+
* .providesService(({ db }) => ({
|
|
464
|
+
* createUser: async (name: string) => {
|
|
465
|
+
* return db.create('user', { name });
|
|
466
|
+
* }
|
|
467
|
+
* }))
|
|
468
|
+
* ```
|
|
469
|
+
*/
|
|
470
|
+
providesService<TNewServices>(
|
|
471
|
+
fn: (context: {
|
|
472
|
+
config: TConfig;
|
|
473
|
+
fragnoConfig: FragnoPublicConfig;
|
|
474
|
+
deps: TDeps & TUsedServices;
|
|
475
|
+
db: AbstractQuery<TSchema>;
|
|
476
|
+
defineService: <T extends Record<string, unknown>>(
|
|
477
|
+
services: WithDatabaseThis<T>,
|
|
478
|
+
) => WithDatabaseThis<T>;
|
|
479
|
+
}) => TNewServices,
|
|
480
|
+
): DatabaseFragmentBuilder<
|
|
481
|
+
TSchema,
|
|
482
|
+
TConfig,
|
|
483
|
+
TDeps,
|
|
484
|
+
TNewServices,
|
|
485
|
+
TUsedServices,
|
|
486
|
+
TProvidedServices
|
|
487
|
+
>;
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Provide a named service that other fragments can use.
|
|
491
|
+
* Functions in the service will have access to DatabaseRequestThisContext via `this` if using `defineService`.
|
|
492
|
+
* You can also pass a service object directly instead of a callback.
|
|
493
|
+
*
|
|
494
|
+
* @example
|
|
495
|
+
* With callback and `this` context:
|
|
496
|
+
* ```ts
|
|
497
|
+
* .providesService("myService", ({ defineService }) => defineService({
|
|
498
|
+
* createUser: function(name: string) {
|
|
499
|
+
* const uow = this.getUnitOfWork(mySchema);
|
|
500
|
+
* return uow.create('user', { name });
|
|
501
|
+
* }
|
|
502
|
+
* }))
|
|
503
|
+
* ```
|
|
504
|
+
*
|
|
505
|
+
* With callback, no `this` context:
|
|
506
|
+
* ```ts
|
|
507
|
+
* .providesService("myService", ({ db }) => ({
|
|
508
|
+
* createUser: async (name: string) => {
|
|
509
|
+
* return db.create('user', { name });
|
|
510
|
+
* }
|
|
511
|
+
* }))
|
|
512
|
+
* ```
|
|
513
|
+
*
|
|
514
|
+
* With direct object:
|
|
515
|
+
* ```ts
|
|
516
|
+
* .providesService("myService", {
|
|
517
|
+
* createUser: async (name: string) => { ... }
|
|
518
|
+
* })
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
providesService<TServiceName extends string, TService>(
|
|
522
|
+
serviceName: TServiceName,
|
|
523
|
+
fnOrService:
|
|
524
|
+
| ((context: {
|
|
525
|
+
config: TConfig;
|
|
526
|
+
fragnoConfig: FragnoPublicConfig;
|
|
527
|
+
deps: TDeps & TUsedServices;
|
|
528
|
+
db: AbstractQuery<TSchema>;
|
|
529
|
+
defineService: <T extends Record<string, unknown>>(
|
|
530
|
+
services: WithDatabaseThis<T>,
|
|
531
|
+
) => WithDatabaseThis<T>;
|
|
532
|
+
}) => TService)
|
|
533
|
+
| TService,
|
|
534
|
+
): DatabaseFragmentBuilder<
|
|
535
|
+
TSchema,
|
|
536
|
+
TConfig,
|
|
537
|
+
TDeps,
|
|
538
|
+
TServices,
|
|
539
|
+
TUsedServices,
|
|
540
|
+
TProvidedServices & { [K in TServiceName]: BoundServices<TService> }
|
|
541
|
+
>;
|
|
542
|
+
|
|
543
|
+
providesService<TServiceName extends string, TService>(
|
|
544
|
+
...args:
|
|
545
|
+
| [
|
|
546
|
+
fn: (context: {
|
|
547
|
+
config: TConfig;
|
|
548
|
+
fragnoConfig: FragnoPublicConfig;
|
|
549
|
+
deps: TDeps & TUsedServices;
|
|
550
|
+
db: AbstractQuery<TSchema>;
|
|
551
|
+
defineService: <T extends Record<string, unknown>>(
|
|
552
|
+
services: WithDatabaseThis<T>,
|
|
553
|
+
) => WithDatabaseThis<T>;
|
|
554
|
+
}) => TService,
|
|
555
|
+
]
|
|
556
|
+
| [
|
|
557
|
+
serviceName: TServiceName,
|
|
558
|
+
fnOrService:
|
|
559
|
+
| ((context: {
|
|
560
|
+
config: TConfig;
|
|
561
|
+
fragnoConfig: FragnoPublicConfig;
|
|
562
|
+
deps: TDeps & TUsedServices;
|
|
563
|
+
db: AbstractQuery<TSchema>;
|
|
564
|
+
defineService: <T extends Record<string, unknown>>(
|
|
565
|
+
services: WithDatabaseThis<T>,
|
|
566
|
+
) => WithDatabaseThis<T>;
|
|
567
|
+
}) => TService)
|
|
568
|
+
| TService,
|
|
569
|
+
]
|
|
570
|
+
):
|
|
571
|
+
| DatabaseFragmentBuilder<TSchema, TConfig, TDeps, TService, TUsedServices, TProvidedServices>
|
|
572
|
+
| DatabaseFragmentBuilder<
|
|
573
|
+
TSchema,
|
|
574
|
+
TConfig,
|
|
575
|
+
TDeps,
|
|
576
|
+
TServices,
|
|
577
|
+
TUsedServices,
|
|
578
|
+
TProvidedServices & { [K in TServiceName]: BoundServices<TService> }
|
|
579
|
+
> {
|
|
580
|
+
if (args.length === 1) {
|
|
581
|
+
// Unnamed service - replaces withServices
|
|
582
|
+
const [fn] = args;
|
|
583
|
+
|
|
584
|
+
// Create a callback that takes a single context object (matching #services signature)
|
|
585
|
+
// Note: We don't explicitly type the return to preserve the WithDatabaseThis wrapper
|
|
586
|
+
const servicesCallback = (
|
|
587
|
+
context: {
|
|
588
|
+
config: TConfig;
|
|
589
|
+
fragnoConfig: FragnoPublicConfig;
|
|
590
|
+
deps: TDeps & TUsedServices;
|
|
591
|
+
} & DatabaseFragmentContext<TSchema>,
|
|
592
|
+
) => {
|
|
593
|
+
// defineService provides typing for service functions
|
|
594
|
+
// It expects the input to already have proper 'this' types on functions
|
|
595
|
+
const defineService = <T extends Record<string, unknown>>(
|
|
596
|
+
services: WithDatabaseThis<T>,
|
|
597
|
+
): WithDatabaseThis<T> => services;
|
|
598
|
+
|
|
599
|
+
const services = fn({
|
|
600
|
+
config: context.config,
|
|
601
|
+
fragnoConfig: context.fragnoConfig,
|
|
602
|
+
deps: context.deps,
|
|
603
|
+
db: context.orm,
|
|
604
|
+
defineService,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// Return without casting to preserve the WithDatabaseThis wrapper
|
|
608
|
+
return services;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
return new DatabaseFragmentBuilder<
|
|
612
|
+
TSchema,
|
|
613
|
+
TConfig,
|
|
614
|
+
TDeps,
|
|
615
|
+
TService,
|
|
616
|
+
TUsedServices,
|
|
617
|
+
TProvidedServices
|
|
618
|
+
>({
|
|
619
|
+
name: this.#name,
|
|
620
|
+
schema: this.#schema,
|
|
621
|
+
namespace: this.#namespace,
|
|
622
|
+
dependencies: this.#dependencies,
|
|
623
|
+
// Safe cast: servicesCallback returns WithDatabaseThis<TService> but we store it as TService.
|
|
624
|
+
// At runtime, bindServicesToContext will handle the 'this' binding properly.
|
|
625
|
+
services: servicesCallback as (
|
|
626
|
+
context: {
|
|
627
|
+
config: TConfig;
|
|
628
|
+
fragnoConfig: FragnoPublicConfig;
|
|
629
|
+
deps: TDeps & TUsedServices;
|
|
630
|
+
defineService: <T extends Record<string, unknown>>(
|
|
631
|
+
services: WithDatabaseThis<T>,
|
|
632
|
+
) => WithDatabaseThis<T>;
|
|
633
|
+
} & DatabaseFragmentContext<TSchema>,
|
|
634
|
+
) => TService,
|
|
635
|
+
usedServices: this.#usedServices,
|
|
636
|
+
providedServices: this.#providedServices,
|
|
637
|
+
});
|
|
638
|
+
} else {
|
|
639
|
+
// Named service
|
|
640
|
+
const [serviceName, fnOrService] = args;
|
|
641
|
+
|
|
642
|
+
// Create a callback that provides the full context
|
|
643
|
+
const createService = (
|
|
644
|
+
config: TConfig,
|
|
645
|
+
options: FragnoPublicConfig,
|
|
646
|
+
deps: TDeps & TUsedServices,
|
|
647
|
+
): BoundServices<TService> => {
|
|
648
|
+
const dbContext = this.#createDatabaseContext(
|
|
649
|
+
options,
|
|
650
|
+
this.#schema,
|
|
651
|
+
this.#namespace ?? "",
|
|
652
|
+
this.#name,
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// Check if fnOrService is a function or a direct object
|
|
656
|
+
let implementation: TService;
|
|
657
|
+
if (typeof fnOrService === "function") {
|
|
658
|
+
// It's a callback - call it with context
|
|
659
|
+
// defineService provides typing for service functions
|
|
660
|
+
// It expects the input to already have proper 'this' types on functions
|
|
661
|
+
const defineService = <T extends Record<string, unknown>>(
|
|
662
|
+
services: WithDatabaseThis<T>,
|
|
663
|
+
): WithDatabaseThis<T> => services;
|
|
664
|
+
|
|
665
|
+
// Safe cast: we checked that fnOrService is a function
|
|
666
|
+
implementation = (
|
|
667
|
+
fnOrService as (context: {
|
|
668
|
+
config: TConfig;
|
|
669
|
+
fragnoConfig: FragnoPublicConfig;
|
|
670
|
+
deps: TDeps & TUsedServices;
|
|
671
|
+
db: AbstractQuery<TSchema>;
|
|
672
|
+
defineService: <T extends Record<string, unknown>>(
|
|
673
|
+
services: WithDatabaseThis<T>,
|
|
674
|
+
) => WithDatabaseThis<T>;
|
|
675
|
+
}) => TService
|
|
676
|
+
)({
|
|
677
|
+
config,
|
|
678
|
+
fragnoConfig: options,
|
|
679
|
+
deps,
|
|
680
|
+
db: dbContext.orm,
|
|
681
|
+
defineService,
|
|
682
|
+
});
|
|
683
|
+
} else {
|
|
684
|
+
// It's a direct object
|
|
685
|
+
implementation = fnOrService;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Bind the service implementation so methods have access to serviceContext
|
|
689
|
+
return bindServicesToContext(
|
|
690
|
+
implementation as Record<string, unknown>,
|
|
691
|
+
) as BoundServices<TService>;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// We need to evaluate this immediately to store in providedServices
|
|
695
|
+
// For now, we'll create a placeholder that will be evaluated when fragment is instantiated
|
|
696
|
+
// Actually, we need to defer this until fragment instantiation
|
|
697
|
+
// Let's store a function that creates the service
|
|
698
|
+
return new DatabaseFragmentBuilder<
|
|
699
|
+
TSchema,
|
|
700
|
+
TConfig,
|
|
701
|
+
TDeps,
|
|
702
|
+
TServices,
|
|
703
|
+
TUsedServices,
|
|
704
|
+
TProvidedServices & { [K in TServiceName]: BoundServices<TService> }
|
|
705
|
+
>({
|
|
706
|
+
name: this.#name,
|
|
707
|
+
schema: this.#schema,
|
|
708
|
+
namespace: this.#namespace,
|
|
709
|
+
dependencies: this.#dependencies,
|
|
710
|
+
services: this.#services,
|
|
711
|
+
usedServices: this.#usedServices,
|
|
712
|
+
providedServices: {
|
|
713
|
+
...this.#providedServices,
|
|
714
|
+
[serviceName]: createService,
|
|
715
|
+
} as Record<string, unknown>,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
190
719
|
}
|
|
191
720
|
|
|
192
721
|
export function defineFragmentWithDatabase<TConfig = {}>(
|
|
193
722
|
name: string,
|
|
194
|
-
): DatabaseFragmentBuilder<never, TConfig, {}, {}> {
|
|
195
|
-
return new DatabaseFragmentBuilder<never, TConfig, {}, {}>({
|
|
723
|
+
): DatabaseFragmentBuilder<never, TConfig, {}, {}, {}, {}> {
|
|
724
|
+
return new DatabaseFragmentBuilder<never, TConfig, {}, {}, {}, {}>({
|
|
196
725
|
name,
|
|
197
726
|
});
|
|
198
727
|
}
|