@tanglemedia/svelte-starter-toolbelt 2.0.0-next.0 → 2.0.0-next.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.
@@ -1,2 +1,3 @@
1
1
  export * from './provider/create-directus-service.provider.ts';
2
+ export * from './provider/create-fetch-service.provider.ts';
2
3
  export * from './service.factory.ts';
@@ -1,2 +1,3 @@
1
1
  export * from "./provider/create-directus-service.provider.js";
2
+ export * from "./provider/create-fetch-service.provider.js";
2
3
  export * from "./service.factory.js";
@@ -747,5 +747,6 @@ export declare class DirectusBaseService<T extends AnyObject> extends ServiceAbs
747
747
  type ServiceProviderOpt<T extends AnyObject> = {
748
748
  serviceClass?: (base: ConfigurableServiceClass<T>) => ConfigurableServiceClass<T, DirectusBaseService<T>>;
749
749
  };
750
+ export declare function isDirectusService<T extends AnyObject>(service: ServiceAbstract<T>): service is DirectusBaseService<T>;
750
751
  export declare function createDirectusServiceProvider({ configureService }: ConfiguredApplication): <T extends AnyObject>(collection: string, opt?: Partial<ServiceAbstractOptions> & ServiceProviderOpt<T>) => DirectusBaseService<T>;
751
752
  export {};
@@ -17,6 +17,9 @@ export class DirectusBaseService extends ServiceAbstract {
17
17
  }
18
18
  }
19
19
  // const extender = (...parts: ConfigurableServiceClass[]) => parts.reduce((acc, ext) => ext(acc), DirectusBaseService);
20
+ export function isDirectusService(service) {
21
+ return service instanceof DirectusBaseService;
22
+ }
20
23
  export function createDirectusServiceProvider({ configureService }) {
21
24
  /**
22
25
  * Variable used for internal caching so that services with the same
@@ -0,0 +1,9 @@
1
+ import { ServiceAbstract, type AnyObject, type ConfigurableServiceClass, type ConfiguredApplication, type ServiceAbstractOptions } from '@tanglemedia/svelte-starter-core';
2
+ export declare class BaseService<T extends AnyObject> extends ServiceAbstract<T> {
3
+ }
4
+ type FetchServiceProviderOpt<T extends AnyObject> = {
5
+ serviceClass?: (base: ConfigurableServiceClass<T, BaseService<T>>) => ConfigurableServiceClass<T, BaseService<T>>;
6
+ adapterKey?: string;
7
+ };
8
+ export declare function createFetchServiceProvider({ configureService }: ConfiguredApplication): <T extends AnyObject>(collection: string, opt?: Partial<ServiceAbstractOptions> & FetchServiceProviderOpt<T>) => BaseService<T>;
9
+ export {};
@@ -0,0 +1,18 @@
1
+ import { ServiceAbstract } from '@tanglemedia/svelte-starter-core';
2
+ export class BaseService extends ServiceAbstract {
3
+ }
4
+ export function createFetchServiceProvider({ configureService }) {
5
+ const services = {};
6
+ return (collection, opt) => {
7
+ const k = JSON.stringify([collection, opt]);
8
+ if (services[k]) {
9
+ return services[k];
10
+ }
11
+ const Service = opt?.serviceClass ? opt.serviceClass(BaseService) : BaseService;
12
+ return (services[k] = configureService(Service, {
13
+ path: collection,
14
+ adapterKey: opt?.adapterKey ?? 'fetch',
15
+ ...(opt ?? {})
16
+ }));
17
+ };
18
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AnyObject, ConfiguredApplication, ServiceAbstract, ServiceAbstractOptions } from "@tanglemedia/svelte-starter-core";
2
- import { DirectusBaseService } from "./provider/create-directus-service.provider";
2
+ import { createDirectusServiceProvider } from "./provider/create-directus-service.provider";
3
3
  import { createApiMutations, createApiQueries } from "../utility/query-factory.ts";
4
4
  import type { CamelCase } from "../types/index.ts";
5
5
  import type { CreateFindOneFactory } from "../utility/query/create-find-one-query.svelte.ts";
@@ -28,7 +28,25 @@ type ServiceFactoryReturn<T extends AnyObject, P extends string, S extends Servi
28
28
  }>)>;
29
29
  };
30
30
  /**
31
- * Initial attempt to reduce the number of boiler plating done for directus services
31
+ * The shape of a provider creator a function that accepts a configured app
32
+ * and returns a bound provider. This matches createDirectusServiceProvider,
33
+ * createFetchServiceProvider, or any developer-supplied custom provider.
32
34
  */
33
- export declare function createServiceFactory<Schema extends Record<string, AnyObject>>(app: ConfiguredApplication): <P extends string>(collection: P, opt?: Partial<ServiceAbstractOptions>) => ServiceFactoryReturn<Schema[P], P, DirectusBaseService<Schema[P]>>;
35
+ export type ProviderCreator = (app: ConfiguredApplication) => <T extends AnyObject>(collection: string, opt?: Partial<ServiceAbstractOptions> & Record<string, unknown>) => ServiceAbstract<T>;
36
+ /**
37
+ * Infers the concrete service instance type from a provider creator PC for a
38
+ * given row type T. Walks PC → ReturnType<PC> (bound provider) → infer R from
39
+ * its return → intersect with ServiceAbstract<T> to bind the concrete T slot.
40
+ *
41
+ * Examples:
42
+ * ExtractService<typeof createDirectusServiceProvider, Product> → DirectusBaseService<Product>
43
+ * ExtractService<typeof createFetchServiceProvider, Product> → BaseService<Product>
44
+ */
45
+ export type ExtractService<PC extends ProviderCreator, T extends AnyObject> = ReturnType<PC> extends <_U extends AnyObject>(collection: string, opt?: any) => infer R ? R extends ServiceAbstract<AnyObject> ? R & ServiceAbstract<T> : ServiceAbstract<T> : ServiceAbstract<T>;
46
+ /**
47
+ * Creates a per-schema service factory. Pass a provider creator as the second
48
+ * argument to choose the adapter backing (Directus or fetch). Defaults to
49
+ * createDirectusServiceProvider for full backward compatibility.
50
+ */
51
+ export declare function createServiceFactory<Schema extends Record<string, AnyObject>, PC extends ProviderCreator = typeof createDirectusServiceProvider>(app: ConfiguredApplication, providerCreator?: PC): <P extends string>(collection: P, opt?: Partial<ServiceAbstractOptions>) => ServiceFactoryReturn<Schema[P], P, ExtractService<PC, Schema[P]>>;
34
52
  export {};
@@ -1,35 +1,37 @@
1
- import { createDirectusServiceProvider, DirectusBaseService } from "./provider/create-directus-service.provider";
1
+ import { createDirectusServiceProvider } from "./provider/create-directus-service.provider";
2
2
  import { createApiMutations, createApiQueries } from "../utility/query-factory.js";
3
3
  import camelCase from 'lodash.camelcase';
4
4
  /**
5
- * Initial attempt to reduce the number of boiler plating done for directus services
5
+ * Creates a per-schema service factory. Pass a provider creator as the second
6
+ * argument to choose the adapter backing (Directus or fetch). Defaults to
7
+ * createDirectusServiceProvider for full backward compatibility.
6
8
  */
7
- export function createServiceFactory(app) {
8
- const serviceProvider = createDirectusServiceProvider(app);
9
- // type K = keyof Schema;
10
- // function factory<T extends AnyObject> (collection: Px, opt?: Partial<ServiceAbstractOptions>): ServiceFactoryReturn<T, Px, DirectusBaseService<T>>;
9
+ export function createServiceFactory(app, providerCreator = createDirectusServiceProvider) {
10
+ // Cast to ReturnType<PC> so the generic call below is typed against the
11
+ // concrete bound provider rather than the loose ProviderCreator upper bound.
12
+ const serviceProvider = providerCreator(app);
11
13
  function factory(collection, opt) {
14
+ // Keep as ServiceAbstract<Schema[P]> inside the body to avoid the deferred
15
+ // conditional type (ExtractService) distributing to a union, which would
16
+ // prevent passing service to createApiMutations. The ExtractService cast
17
+ // is applied only on the final return object.
12
18
  const service = serviceProvider(collection, opt);
13
- // console.log(`FACTORY ${collection}`, service)
14
19
  const createQuries = () => createApiQueries({
15
20
  collection,
16
21
  service
17
22
  });
18
23
  const createMutations = () => createApiMutations({
19
24
  collection,
20
- service
25
+ // service is ServiceAbstract<Schema[P]>; cast to satisfy the id constraint
26
+ // required by createApiMutations. Safe: every concrete service impl handles
27
+ // id-bearing entities at runtime.
28
+ service: service
21
29
  });
22
- // todo: Do the same for mutations
23
30
  const [find, findOne, findInfinite] = createQuries();
24
31
  const [createOne, updateOne, deleteOne] = createMutations();
25
32
  const k1 = camelCase(`find_${collection}`);
26
33
  const k2 = camelCase(`find_${collection}_by_id`);
27
34
  const k3 = camelCase(`find_${collection}_infinite`);
28
- // const queries = {
29
- // [k1]: find,
30
- // [k2]: findOne,
31
- // [k3]: findInfinite,
32
- // };
33
35
  const m1 = camelCase(`create_${collection}`);
34
36
  const m2 = camelCase(`update_${collection}`);
35
37
  const m3 = camelCase(`delete_${collection}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanglemedia/svelte-starter-toolbelt",
3
- "version": "2.0.0-next.0",
3
+ "version": "2.0.0-next.2",
4
4
  "files": [
5
5
  "src",
6
6
  "dist",
@@ -10,17 +10,21 @@
10
10
  "sideEffects": [
11
11
  "**/*.css"
12
12
  ],
13
- "svelte": "./src/index.ts",
14
- "types": "./src/index.ts",
13
+ "module": "./dist/index.d.ts",
14
+ "svelte": "./dist/index.js",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
18
- "types": "./src/index.ts",
19
- "svelte": "./src/index.ts"
18
+ "types": "./dist/index.d.ts",
19
+ "svelte": "./dist/index.js",
20
+ "import": "./dist/index.js",
21
+ "default": "./dist/index.js"
20
22
  },
21
23
  "./server": {
22
- "types": "./src/lib/server/index.ts",
23
- "svelte": "./src/lib/server/index.ts"
24
+ "types": "./dist/server/index.d.ts",
25
+ "svelte": "./dist/server/index.js",
26
+ "import": "./dist/server/index.js",
27
+ "default": "./dist/server/index.js"
24
28
  }
25
29
  },
26
30
  "peerDependencies": {
@@ -58,9 +62,10 @@
58
62
  "typescript": "^5.9.3",
59
63
  "typescript-eslint": "^8.54.0",
60
64
  "vite": "^8.0.8",
65
+ "msw": "^2.12.7",
61
66
  "vitest": "^4.1.4",
62
- "@tanglemedia/svelte-starter-core": "4.0.0-next.0",
63
- "@tanglemedia/svelte-starter-types": "3.0.0-next.0"
67
+ "@tanglemedia/svelte-starter-types": "3.0.0-next.0",
68
+ "@tanglemedia/svelte-starter-core": "4.0.0-next.0"
64
69
  },
65
70
  "keywords": [
66
71
  "svelte"
@@ -1,2 +1,3 @@
1
1
  export * from './provider/create-directus-service.provider.ts';
2
- export * from './service.factory.ts';
2
+ export * from './provider/create-fetch-service.provider.ts';
3
+ export * from './service.factory.ts';
@@ -1,19 +1,17 @@
1
1
  import {
2
- readMe,
3
- type AuthenticationClient,
4
- type DirectusClient,
5
- type RestClient,
6
- type RestCommand
2
+ readMe,
3
+ type AuthenticationClient,
4
+ type DirectusClient,
5
+ type RestClient,
6
+ type RestCommand
7
7
  } from '@directus/sdk';
8
8
  import {
9
- ServiceAbstract,
10
- type ServiceAdapterAbstract,
11
- type AnyObject,
12
- type ApiAdapterHandle,
13
- type ConfigurableServiceClass,
14
- type ConfiguredApplication,
15
- type HandleEvent,
16
- type ServiceAbstractOptions
9
+ ServiceAbstract,
10
+ type AnyObject,
11
+ type ConfigurableServiceClass,
12
+ type ConfiguredApplication,
13
+ type HandleEvent,
14
+ type ServiceAbstractOptions
17
15
  } from '@tanglemedia/svelte-starter-core';
18
16
  export class DirectusBaseService<T extends AnyObject> extends ServiceAbstract<T> {
19
17
  public getDirectus() {
@@ -47,6 +45,12 @@ type ServiceProviderOpt<T extends AnyObject> = {
47
45
 
48
46
  // const extender = (...parts: ConfigurableServiceClass[]) => parts.reduce((acc, ext) => ext(acc), DirectusBaseService);
49
47
 
48
+ export function isDirectusService<T extends AnyObject>(
49
+ service: ServiceAbstract<T>
50
+ ): service is DirectusBaseService<T> {
51
+ return service instanceof DirectusBaseService;
52
+ }
53
+
50
54
  export function createDirectusServiceProvider({ configureService }: ConfiguredApplication) {
51
55
 
52
56
  /**
@@ -0,0 +1,37 @@
1
+ import {
2
+ ServiceAbstract,
3
+ type AnyObject,
4
+ type ConfigurableServiceClass,
5
+ type ConfiguredApplication,
6
+ type ServiceAbstractOptions
7
+ } from '@tanglemedia/svelte-starter-core';
8
+
9
+ export class BaseService<T extends AnyObject> extends ServiceAbstract<T> {}
10
+
11
+ type FetchServiceProviderOpt<T extends AnyObject> = {
12
+ serviceClass?: (base: ConfigurableServiceClass<T, BaseService<T>>) => ConfigurableServiceClass<T, BaseService<T>>;
13
+ adapterKey?: string;
14
+ };
15
+
16
+ export function createFetchServiceProvider({ configureService }: ConfiguredApplication) {
17
+ const services: Record<string, unknown> = {};
18
+
19
+ return <T extends AnyObject>(
20
+ collection: string,
21
+ opt?: Partial<ServiceAbstractOptions> & FetchServiceProviderOpt<T>
22
+ ): BaseService<T> => {
23
+ const k = JSON.stringify([collection, opt]);
24
+
25
+ if (services[k]) {
26
+ return services[k] as BaseService<T>;
27
+ }
28
+
29
+ const Service = opt?.serviceClass ? opt.serviceClass(BaseService) : BaseService<T>;
30
+
31
+ return (services[k] = configureService<T, BaseService<T>>(Service, {
32
+ path: collection,
33
+ adapterKey: opt?.adapterKey ?? 'fetch',
34
+ ...(opt ?? {})
35
+ })) as BaseService<T>;
36
+ };
37
+ }
@@ -0,0 +1,284 @@
1
+ import { setupServer } from 'msw/node';
2
+ import { http, HttpResponse } from 'msw';
3
+ import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest';
4
+ import { ApiAdapterProvider } from '@tanglemedia/svelte-starter-core';
5
+ import type { AnyObject, ConfiguredApplication, ServiceAbstractOptions } from '@tanglemedia/svelte-starter-core';
6
+ import { createServiceFactory } from './service.factory';
7
+ import { createDirectusServiceProvider, DirectusBaseService, isDirectusService } from './provider/create-directus-service.provider';
8
+ import { BaseService, createFetchServiceProvider } from './provider/create-fetch-service.provider';
9
+
10
+ vi.mock('$env/dynamic/public', () => ({ env: {} }));
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Mock data
14
+ // ---------------------------------------------------------------------------
15
+
16
+ type Product = { id: number; name: string; price: number };
17
+ type Order = { id: number; productId: number; quantity: number };
18
+
19
+ const products: Product[] = [
20
+ { id: 1, name: 'Widget A', price: 9.99 },
21
+ { id: 2, name: 'Widget B', price: 19.99 }
22
+ ];
23
+
24
+ const orders: Order[] = [
25
+ { id: 1, productId: 1, quantity: 2 },
26
+ { id: 2, productId: 2, quantity: 1 }
27
+ ];
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // MSW server
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const BASE = 'https://api.test.local/api';
34
+
35
+ const handlers = [
36
+ http.get(`${BASE}/product`, () => HttpResponse.json({ data: products })),
37
+ http.get(`${BASE}/product/1`, () => HttpResponse.json({ data: products[0] })),
38
+ http.get(`${BASE}/product/99`, () => HttpResponse.json({}, { status: 404 })),
39
+ http.post(`${BASE}/product`, async ({ request }) => {
40
+ const body = (await request.json()) as Omit<Product, 'id'>;
41
+ const created: Product = { id: products.length + 1, ...body };
42
+ products.push(created);
43
+ return HttpResponse.json({ data: created });
44
+ }),
45
+ http.patch(`${BASE}/product/1`, async ({ request }) => {
46
+ const body = (await request.json()) as Partial<Product>;
47
+ const updated = { ...products[0], ...body };
48
+ products[0] = updated;
49
+ return HttpResponse.json({ data: updated });
50
+ }),
51
+ http.delete(`${BASE}/product/1`, () => HttpResponse.json({ data: { id: 1 } })),
52
+
53
+ http.get(`${BASE}/order`, () => HttpResponse.json({ data: orders }))
54
+ ];
55
+
56
+ const server = setupServer(...handlers);
57
+
58
+ beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
59
+ afterAll(() => server.close());
60
+ afterEach(() => server.resetHandlers());
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Minimal ConfiguredApplication backed by the real fetch adapter
64
+ // ---------------------------------------------------------------------------
65
+
66
+ type Schema = {
67
+ product: Product;
68
+ order: Order;
69
+ };
70
+
71
+ const fetchConfig = {
72
+ api: {
73
+ default: 'fetch',
74
+ adapters: {
75
+ fetch: {
76
+ baseUrl: 'https://api.test.local',
77
+ configuration: { pathPrefix: '/api' }
78
+ }
79
+ }
80
+ }
81
+ };
82
+
83
+ // Build a minimal ConfiguredApplication that uses the real ApiAdapterProvider
84
+ // with a static in-memory config — mirrors the pattern from core service tests.
85
+ const mockConfigStore = {
86
+ config: (key: string): unknown => {
87
+ const cfg: Record<string, unknown> = {
88
+ 'api.default': fetchConfig.api.default,
89
+ 'api.adapters.fetch': fetchConfig.api.adapters.fetch
90
+ };
91
+ return cfg[key];
92
+ }
93
+ };
94
+
95
+ const adapterProvider = new ApiAdapterProvider(async () => mockConfigStore as any);
96
+
97
+ const mockApp: ConfiguredApplication = {
98
+ configureService: <T extends AnyObject, S>(
99
+ ServiceClass: new (p: typeof adapterProvider, o: ServiceAbstractOptions) => S,
100
+ opt: ServiceAbstractOptions
101
+ ) => new ServiceClass(adapterProvider, opt) as S
102
+ } as unknown as ConfiguredApplication;
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Tests
106
+ // ---------------------------------------------------------------------------
107
+
108
+ describe('createServiceFactory — structure', () => {
109
+ test('factory function is returned', () => {
110
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
111
+ expect(typeof createService).toBe('function');
112
+ });
113
+
114
+ test('factory generates camelCase query method names for a collection', () => {
115
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
116
+ const productService = createService('product');
117
+
118
+ expect(typeof productService.findProduct).toBe('function');
119
+ expect(typeof productService.findProductById).toBe('function');
120
+ expect(typeof productService.findProductInfinite).toBe('function');
121
+ });
122
+
123
+ test('factory generates camelCase mutation method names for a collection', () => {
124
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
125
+ const productService = createService('product');
126
+
127
+ expect(typeof productService.createProduct).toBe('function');
128
+ expect(typeof productService.updateProduct).toBe('function');
129
+ expect(typeof productService.deleteProduct).toBe('function');
130
+ });
131
+
132
+ test('factory exposes createQuries and createMutations helpers', () => {
133
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
134
+ const productService = createService('product');
135
+
136
+ expect(typeof productService.createQuries).toBe('function');
137
+ expect(typeof productService.createMutations).toBe('function');
138
+ });
139
+
140
+ test('method names are scoped to the collection — order vs product', () => {
141
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
142
+ const orderService = createService('order');
143
+
144
+ expect(typeof orderService.findOrder).toBe('function');
145
+ expect(typeof orderService.findOrderById).toBe('function');
146
+ expect(typeof orderService.createOrder).toBe('function');
147
+
148
+ // order factory should NOT have product methods
149
+ expect((orderService as any).findProduct).toBeUndefined();
150
+ });
151
+
152
+ test('each call to factory returns a fresh service object', () => {
153
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
154
+ const a = createService('product');
155
+ const b = createService('order');
156
+ expect(a).not.toBe(b);
157
+ });
158
+ });
159
+
160
+ describe('createServiceFactory — service class types', () => {
161
+ test('fetch provider produces a BaseService instance', () => {
162
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
163
+ const { service } = createService('product');
164
+
165
+ expect(service).toBeInstanceOf(BaseService);
166
+ });
167
+
168
+ test('fetch provider service is NOT a DirectusBaseService', () => {
169
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
170
+ const { service } = createService('product');
171
+
172
+ expect(service).not.toBeInstanceOf(DirectusBaseService);
173
+ });
174
+
175
+ test('directus provider produces a DirectusBaseService instance', () => {
176
+ const createService = createServiceFactory<Schema>(mockApp, createDirectusServiceProvider);
177
+ const { service } = createService('product');
178
+
179
+ expect(service).toBeInstanceOf(DirectusBaseService);
180
+ });
181
+
182
+ test('default (no second arg) produces a DirectusBaseService instance', () => {
183
+ const createService = createServiceFactory<Schema>(mockApp);
184
+ const { service } = createService('product');
185
+
186
+ expect(service).toBeInstanceOf(DirectusBaseService);
187
+ });
188
+ });
189
+
190
+ describe('isDirectusService guard', () => {
191
+ test('returns true for DirectusBaseService', () => {
192
+ const createService = createServiceFactory<Schema>(mockApp, createDirectusServiceProvider);
193
+ const { service } = createService('product');
194
+
195
+ expect(isDirectusService(service)).toBe(true);
196
+ });
197
+
198
+ test('returns false for BaseService', () => {
199
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
200
+ const { service } = createService('product');
201
+
202
+ expect(isDirectusService(service)).toBe(false);
203
+ });
204
+
205
+ test('guard narrows type — getDirectus is accessible after guard', () => {
206
+ const createService = createServiceFactory<Schema>(mockApp, createDirectusServiceProvider);
207
+ const { service } = createService('product');
208
+
209
+ if (isDirectusService(service)) {
210
+ // TypeScript should see service as DirectusBaseService here
211
+ expect(typeof service.getDirectus).toBe('function');
212
+ } else {
213
+ throw new Error('Expected service to be a DirectusBaseService');
214
+ }
215
+ });
216
+ });
217
+
218
+ describe('createServiceFactory — fetch provider HTTP calls', () => {
219
+ test('service.find() returns all products', async () => {
220
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
221
+ const { service } = createService('product');
222
+
223
+ const { data } = await service.find({});
224
+
225
+ expect(data).toHaveLength(products.length);
226
+ expect(data[0]).toMatchObject({ name: 'Widget A' });
227
+ });
228
+
229
+ test('service.findOne() returns a single product by id', async () => {
230
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
231
+ const { service } = createService('product');
232
+
233
+ const { data } = await service.findOne(1);
234
+
235
+ expect(data).toMatchObject({ id: 1, name: 'Widget A' });
236
+ });
237
+
238
+ test('service.create() posts a new product and returns it', async () => {
239
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
240
+ const { service } = createService('product');
241
+
242
+ const { data } = await service.create({ name: 'Widget C', price: 29.99 });
243
+
244
+ expect(data).toMatchObject({ name: 'Widget C', price: 29.99 });
245
+ expect(data).toHaveProperty('id');
246
+ });
247
+
248
+ test('service.update() patches an existing product', async () => {
249
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
250
+ const { service } = createService('product');
251
+
252
+ const { data } = await service.update(1, { name: 'Widget A — Updated' });
253
+
254
+ expect(data).toMatchObject({ id: 1, name: 'Widget A — Updated' });
255
+ });
256
+
257
+ test('service.delete() removes a product', async () => {
258
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
259
+ const { service } = createService('product');
260
+
261
+ const { data } = await service.delete(1);
262
+
263
+ expect(data).toMatchObject({ id: 1 });
264
+ });
265
+
266
+ test('the same service instance is reused for identical collection + options', () => {
267
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
268
+ const a = createService('product');
269
+ const b = createService('product');
270
+
271
+ // createFetchServiceProvider caches by [collection, opt] key
272
+ expect(a.service).toBe(b.service);
273
+ });
274
+
275
+ test('order service can independently fetch orders', async () => {
276
+ const createService = createServiceFactory<Schema, typeof createFetchServiceProvider>(mockApp, createFetchServiceProvider);
277
+ const { service } = createService('order');
278
+
279
+ const { data } = await service.find({});
280
+
281
+ expect(data).toHaveLength(orders.length);
282
+ expect(data[0]).toMatchObject({ productId: 1 });
283
+ });
284
+ });
@@ -1,5 +1,5 @@
1
1
  import type { AnyObject, ConfiguredApplication, ServiceAbstract, ServiceAbstractOptions } from "@tanglemedia/svelte-starter-core";
2
- import { createDirectusServiceProvider, DirectusBaseService } from "./provider/create-directus-service.provider";
2
+ import { createDirectusServiceProvider } from "./provider/create-directus-service.provider";
3
3
  import { createApiMutations, createApiQueries } from "../utility/query-factory.ts";
4
4
  import camelCase from 'lodash.camelcase';
5
5
  import type { CamelCase } from "../types/index.ts";
@@ -24,7 +24,6 @@ type MutationFactories<T extends AnyObject, P extends string> = {
24
24
  } & {
25
25
  [k in CamelCase<`delete_${P}`>]: CreateDeleteMutationFactory<any>;
26
26
  }
27
- // type QueryFactories<P extends string> = Record<CamelCase<`find_${P}`>, any>;
28
27
 
29
28
  type ServiceFactoryReturn<
30
29
  T extends AnyObject,
@@ -37,18 +36,62 @@ type ServiceFactoryReturn<
37
36
  };
38
37
 
39
38
  /**
40
- * Initial attempt to reduce the number of boiler plating done for directus services
39
+ * The shape of a provider creator a function that accepts a configured app
40
+ * and returns a bound provider. This matches createDirectusServiceProvider,
41
+ * createFetchServiceProvider, or any developer-supplied custom provider.
41
42
  */
42
- export function createServiceFactory<Schema extends Record<string, AnyObject>>(app: ConfiguredApplication) {
43
- const serviceProvider = createDirectusServiceProvider(app);
43
+ export type ProviderCreator = (
44
+ app: ConfiguredApplication,
45
+ ) => <T extends AnyObject>(
46
+ collection: string,
47
+ opt?: Partial<ServiceAbstractOptions> & Record<string, unknown>,
48
+ ) => ServiceAbstract<T>;
44
49
 
45
- // type K = keyof Schema;
50
+ /**
51
+ * Infers the concrete service instance type from a provider creator PC for a
52
+ * given row type T. Walks PC → ReturnType<PC> (bound provider) → infer R from
53
+ * its return → intersect with ServiceAbstract<T> to bind the concrete T slot.
54
+ *
55
+ * Examples:
56
+ * ExtractService<typeof createDirectusServiceProvider, Product> → DirectusBaseService<Product>
57
+ * ExtractService<typeof createFetchServiceProvider, Product> → BaseService<Product>
58
+ */
59
+ export type ExtractService<PC extends ProviderCreator, T extends AnyObject> =
60
+ ReturnType<PC> extends <_U extends AnyObject>(
61
+ collection: string,
62
+ opt?: any,
63
+ ) => infer R
64
+ ? R extends ServiceAbstract<AnyObject>
65
+ ? R & ServiceAbstract<T>
66
+ : ServiceAbstract<T>
67
+ : ServiceAbstract<T>;
46
68
 
47
- // function factory<T extends AnyObject> (collection: Px, opt?: Partial<ServiceAbstractOptions>): ServiceFactoryReturn<T, Px, DirectusBaseService<T>>;
48
- function factory<P extends string>(collection: P, opt?: Partial<ServiceAbstractOptions>): ServiceFactoryReturn<Schema[P], P, DirectusBaseService<Schema[P]>> {
49
- const service = serviceProvider<Schema[P]>(collection as string, opt);
69
+ /**
70
+ * Creates a per-schema service factory. Pass a provider creator as the second
71
+ * argument to choose the adapter backing (Directus or fetch). Defaults to
72
+ * createDirectusServiceProvider for full backward compatibility.
73
+ */
74
+ export function createServiceFactory<
75
+ Schema extends Record<string, AnyObject>,
76
+ PC extends ProviderCreator = typeof createDirectusServiceProvider,
77
+ >(
78
+ app: ConfiguredApplication,
79
+ providerCreator: PC = createDirectusServiceProvider as unknown as PC,
80
+ ) {
81
+ // Cast to ReturnType<PC> so the generic call below is typed against the
82
+ // concrete bound provider rather than the loose ProviderCreator upper bound.
83
+ const serviceProvider = providerCreator(app) as ReturnType<PC>;
84
+
85
+ function factory<P extends string>(
86
+ collection: P,
87
+ opt?: Partial<ServiceAbstractOptions>,
88
+ ): ServiceFactoryReturn<Schema[P], P, ExtractService<PC, Schema[P]>> {
89
+ // Keep as ServiceAbstract<Schema[P]> inside the body to avoid the deferred
90
+ // conditional type (ExtractService) distributing to a union, which would
91
+ // prevent passing service to createApiMutations. The ExtractService cast
92
+ // is applied only on the final return object.
93
+ const service = serviceProvider<Schema[P]>(collection, opt);
50
94
 
51
- // console.log(`FACTORY ${collection}`, service)
52
95
  const createQuries = () => createApiQueries<Schema[P]>({
53
96
  collection,
54
97
  service
@@ -56,21 +99,18 @@ export function createServiceFactory<Schema extends Record<string, AnyObject>>(a
56
99
 
57
100
  const createMutations = () => createApiMutations<Schema[P] & { id: string | number; }>({
58
101
  collection,
59
- service
102
+ // service is ServiceAbstract<Schema[P]>; cast to satisfy the id constraint
103
+ // required by createApiMutations. Safe: every concrete service impl handles
104
+ // id-bearing entities at runtime.
105
+ service: service as ServiceAbstract<Schema[P] & { id: string | number }>
60
106
  });
61
107
 
62
- // todo: Do the same for mutations
63
108
  const [find, findOne, findInfinite] = createQuries();
64
109
  const [createOne, updateOne, deleteOne] = createMutations();
65
110
 
66
- const k1: CamelCase<`find_${P}`> = camelCase(`find_${collection}`) as CamelCase<`find_${P}`>
67
- const k2: CamelCase<`find_${P}_by_id`> = camelCase(`find_${collection}_by_id`) as CamelCase<`find_${P}_by_id`>
68
- const k3: CamelCase<`find_${P}_infinite`> = camelCase(`find_${collection}_infinite`) as CamelCase<`find_${P}_infinite`>
69
- // const queries = {
70
- // [k1]: find,
71
- // [k2]: findOne,
72
- // [k3]: findInfinite,
73
- // };
111
+ const k1: CamelCase<`find_${P}`> = camelCase(`find_${collection}`) as CamelCase<`find_${P}`>;
112
+ const k2: CamelCase<`find_${P}_by_id`> = camelCase(`find_${collection}_by_id`) as CamelCase<`find_${P}_by_id`>;
113
+ const k3: CamelCase<`find_${P}_infinite`> = camelCase(`find_${collection}_infinite`) as CamelCase<`find_${P}_infinite`>;
74
114
 
75
115
  const m1: CamelCase<`create_${P}`> = camelCase(`create_${collection}`) as CamelCase<`create_${P}`>;
76
116
  const m2: CamelCase<`update_${P}`> = camelCase(`update_${collection}`) as CamelCase<`update_${P}`>;
@@ -86,8 +126,8 @@ export function createServiceFactory<Schema extends Record<string, AnyObject>>(a
86
126
  [m1]: createOne,
87
127
  [m2]: updateOne,
88
128
  [m3]: deleteOne,
89
- } as ServiceFactoryReturn<Schema[P], P, DirectusBaseService<Schema[P]>>;
129
+ } as ServiceFactoryReturn<Schema[P], P, ExtractService<PC, Schema[P]>>;
90
130
  }
91
131
 
92
132
  return factory;
93
- }
133
+ }