@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.
- package/dist/service/index.d.ts +1 -0
- package/dist/service/index.js +1 -0
- package/dist/service/provider/create-directus-service.provider.d.ts +1 -0
- package/dist/service/provider/create-directus-service.provider.js +3 -0
- package/dist/service/provider/create-fetch-service.provider.d.ts +9 -0
- package/dist/service/provider/create-fetch-service.provider.js +18 -0
- package/dist/service/service.factory.d.ts +21 -3
- package/dist/service/service.factory.js +16 -14
- package/package.json +14 -9
- package/src/lib/service/index.ts +2 -1
- package/src/lib/service/provider/create-directus-service.provider.ts +17 -13
- package/src/lib/service/provider/create-fetch-service.provider.ts +37 -0
- package/src/lib/service/service.factory.test.ts +284 -0
- package/src/lib/service/service.factory.ts +62 -22
package/dist/service/index.d.ts
CHANGED
package/dist/service/index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
9
|
-
//
|
|
10
|
-
|
|
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.
|
|
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
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"module": "./dist/index.d.ts",
|
|
14
|
+
"svelte": "./dist/index.js",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"types": "./
|
|
19
|
-
"svelte": "./
|
|
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": "./
|
|
23
|
-
"svelte": "./
|
|
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-
|
|
63
|
-
"@tanglemedia/svelte-starter-
|
|
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"
|
package/src/lib/service/index.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
readMe,
|
|
3
|
+
type AuthenticationClient,
|
|
4
|
+
type DirectusClient,
|
|
5
|
+
type RestClient,
|
|
6
|
+
type RestCommand
|
|
7
7
|
} from '@directus/sdk';
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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,
|
|
129
|
+
} as ServiceFactoryReturn<Schema[P], P, ExtractService<PC, Schema[P]>>;
|
|
90
130
|
}
|
|
91
131
|
|
|
92
132
|
return factory;
|
|
93
|
-
}
|
|
133
|
+
}
|