svelte-ag 1.2.8 → 1.3.1
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/api/form.svelte.d.ts +7 -10
- package/dist/api/form.svelte.d.ts.map +1 -1
- package/dist/api/form.svelte.js +6 -9
- package/dist/api/query/entrypoint.svelte.d.ts +14 -6
- package/dist/api/query/entrypoint.svelte.d.ts.map +1 -1
- package/dist/api/query/entrypoint.svelte.js +11 -5
- package/dist/api/query/entrypoint.unit.test.js +23 -25
- package/dist/api/query/query.svelte.d.ts +4 -5
- package/dist/api/query/query.svelte.d.ts.map +1 -1
- package/dist/api/query/query.svelte.js +8 -12
- package/dist/api/query/query.unit.test.js +21 -30
- package/dist/api/query/utils.svelte.d.ts +8 -2
- package/dist/api/query/utils.svelte.d.ts.map +1 -1
- package/dist/api/query/utils.svelte.js +5 -4
- package/dist/bin/build-tailwind-manifest/build-tailwind-manifest.unit.test.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/build-tailwind-manifest.unit.test.js +386 -0
- package/dist/bin/build-tailwind-manifest/cli.d.ts +2 -0
- package/dist/bin/build-tailwind-manifest/cli.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/cli.js +122 -0
- package/dist/bin/build-tailwind-manifest/graph.d.ts +9 -0
- package/dist/bin/build-tailwind-manifest/graph.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/graph.js +405 -0
- package/dist/bin/build-tailwind-manifest/index.d.ts +1 -0
- package/dist/bin/build-tailwind-manifest/index.js +6 -0
- package/dist/bin/build-tailwind-manifest/manifest-generator.d.ts +7 -0
- package/dist/bin/build-tailwind-manifest/manifest-generator.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/manifest-generator.js +85 -0
- package/dist/bin/build-tailwind-manifest/path-utils.d.ts +6 -0
- package/dist/bin/build-tailwind-manifest/path-utils.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/path-utils.js +41 -0
- package/dist/bin/build-tailwind-manifest/types.d.ts +15 -0
- package/dist/bin/build-tailwind-manifest/types.d.ts.map +1 -0
- package/dist/bin/build-tailwind-manifest/types.js +1 -0
- package/dist/index.d.ts +29 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/tailwind-sources.manifest.jsonc +193 -3
- package/dist/vite/vite-plugin-component-source-collector.d.ts.map +1 -1
- package/dist/vite/vite-plugin-component-source-collector.js +1 -6
- package/package.json +7 -5
- package/src/lib/api/form.svelte.ts +27 -34
- package/src/lib/api/query/entrypoint.svelte.ts +28 -11
- package/src/lib/api/query/entrypoint.unit.test.ts +25 -27
- package/src/lib/api/query/query.svelte.ts +18 -17
- package/src/lib/api/query/query.unit.test.ts +25 -35
- package/src/lib/api/query/utils.svelte.ts +5 -4
- package/src/lib/bin/build-tailwind-manifest/build-tailwind-manifest.unit.test.ts +591 -0
- package/src/lib/bin/build-tailwind-manifest/cli.ts +147 -0
- package/src/lib/bin/build-tailwind-manifest/graph.ts +521 -0
- package/src/lib/bin/build-tailwind-manifest/index.ts +8 -0
- package/src/lib/bin/build-tailwind-manifest/manifest-generator.ts +119 -0
- package/src/lib/bin/build-tailwind-manifest/path-utils.ts +53 -0
- package/src/lib/bin/build-tailwind-manifest/types.ts +18 -0
- package/src/lib/vite/vite-plugin-component-source-collector.ts +1 -6
- package/dist/bin/build-tailwind-manifest.d.ts +0 -15
- package/dist/bin/build-tailwind-manifest.d.ts.map +0 -1
- package/dist/bin/build-tailwind-manifest.js +0 -581
- package/dist/bin/build-tailwind-manifest.unit.test.d.ts.map +0 -1
- package/dist/bin/build-tailwind-manifest.unit.test.js +0 -208
- package/src/lib/bin/build-tailwind-manifest.ts +0 -757
- package/src/lib/bin/build-tailwind-manifest.unit.test.ts +0 -309
- /package/dist/bin/{build-tailwind-manifest.unit.test.d.ts → build-tailwind-manifest/build-tailwind-manifest.unit.test.d.ts} +0 -0
|
@@ -6,12 +6,11 @@ import type { SuperForm } from 'sveltekit-superforms';
|
|
|
6
6
|
import { valibot } from 'sveltekit-superforms/adapters';
|
|
7
7
|
import type {
|
|
8
8
|
ApiRequestFunction,
|
|
9
|
-
HTTPMethod,
|
|
10
9
|
ApiEndpoints,
|
|
11
10
|
ApiInput,
|
|
12
11
|
ApiSuccessBody,
|
|
13
12
|
ApiErrorBody,
|
|
14
|
-
|
|
13
|
+
ApiEndpointContract
|
|
15
14
|
} from 'ts-ag';
|
|
16
15
|
import { safeParse, safeParseAsync } from 'valibot';
|
|
17
16
|
|
|
@@ -51,22 +50,19 @@ type FormErrorBody<
|
|
|
51
50
|
/**
|
|
52
51
|
* Creates a strongly-typed form factory for an API schema.
|
|
53
52
|
*
|
|
54
|
-
* Call the returned function with `{
|
|
53
|
+
* Call the returned function with `{ endpoint, ... }` to get a `SuperForm`
|
|
55
54
|
* that:
|
|
56
55
|
* - Validates using the Valibot schema for the given endpoint.
|
|
57
56
|
* - Submits via the provided `request` function on each valid update.
|
|
58
57
|
* - Maps API errors to `sveltekit-superforms` field errors / messages.
|
|
59
58
|
* - Optionally two-way binds external state through the `bind` adapter.
|
|
60
59
|
*/
|
|
61
|
-
export type ApiRequestForm<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
>
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
/** HTTP method used to select a schema and to call `request(path, method, data)` */
|
|
69
|
-
method: Method;
|
|
60
|
+
export type ApiRequestForm<API extends ApiEndpoints> = <
|
|
61
|
+
Path extends API['path'],
|
|
62
|
+
Method extends Extract<API, { path: Path }>['method']
|
|
63
|
+
>(a: {
|
|
64
|
+
/** Endpoint contract used to select a schema and submit the request. */
|
|
65
|
+
endpoint: ApiEndpointContract<API, Path, Method>;
|
|
70
66
|
|
|
71
67
|
/**
|
|
72
68
|
* Optional lifecycle hooks for consumers.
|
|
@@ -127,37 +123,30 @@ export type ApiRequestForm<
|
|
|
127
123
|
* If you pass `onSubmit` / `onUpdate` here it will override the defaults in this helper.
|
|
128
124
|
*/
|
|
129
125
|
formProps?: Parameters<typeof superForm<ValidInput<API, Path, Method>>>[1];
|
|
130
|
-
|
|
131
|
-
// /**
|
|
132
|
-
// * Schema override to be used in place of the schemas from the creation of
|
|
133
|
-
// * the form function
|
|
134
|
-
// */
|
|
135
|
-
// schema?: NonNullable<Schemas[Path]>[Method];
|
|
136
126
|
}) => SuperForm<ValidInput<API, Path, Method>>;
|
|
137
127
|
|
|
138
128
|
/**
|
|
139
129
|
* Build an endpoint-specific Superforms factory.
|
|
140
130
|
*
|
|
141
|
-
* @param
|
|
142
|
-
* @param request An API request function that performs `(path, method, data)` and returns a fetch-like `Response`.
|
|
131
|
+
* @param request An API request function that performs `(endpoint, data)` and returns a fetch-like `Response`.
|
|
143
132
|
*
|
|
144
|
-
* @returns A function that creates a `SuperForm` for a particular
|
|
133
|
+
* @returns A function that creates a `SuperForm` for a particular endpoint contract.
|
|
145
134
|
*/
|
|
146
|
-
export function createFormFunction<API extends ApiEndpoints>(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
): ApiRequestForm<API> {
|
|
150
|
-
return ({ path, method, actions, defaultValue, formProps, bind }) => {
|
|
151
|
-
const schema = schemas[path]?.[method];
|
|
152
|
-
if (schema === undefined) throw new Error('Invalid schema for form');
|
|
135
|
+
export function createFormFunction<API extends ApiEndpoints>(request: ApiRequestFunction<API>): ApiRequestForm<API> {
|
|
136
|
+
return ({ endpoint, actions, defaultValue, formProps, bind }) => {
|
|
137
|
+
const schema = endpoint.schema;
|
|
153
138
|
|
|
154
139
|
const defaultFormData = defaults(defaultValue, valibot(schema));
|
|
155
140
|
const boundFormData =
|
|
156
141
|
bind && schema.async === false
|
|
157
|
-
? (safeParse(schema, bind.get(defaultFormData)).output as ValidInput<
|
|
142
|
+
? (safeParse(schema, bind.get(defaultFormData)).output as ValidInput<
|
|
143
|
+
API,
|
|
144
|
+
typeof endpoint.path,
|
|
145
|
+
typeof endpoint.method
|
|
146
|
+
>)
|
|
158
147
|
: defaultFormData;
|
|
159
148
|
|
|
160
|
-
const form = superForm<ValidInput<API, typeof path, typeof method>>(boundFormData, {
|
|
149
|
+
const form = superForm<ValidInput<API, typeof endpoint.path, typeof endpoint.method>>(boundFormData, {
|
|
161
150
|
SPA: true,
|
|
162
151
|
resetForm: true,
|
|
163
152
|
applyAction: false, // Prevents the form redirecting to the same page on submit
|
|
@@ -186,10 +175,10 @@ export function createFormFunction<API extends ApiEndpoints>(
|
|
|
186
175
|
if (!props.form.valid) return;
|
|
187
176
|
|
|
188
177
|
// console.log('onUpdate: sending data', form.data);
|
|
189
|
-
const res = await request(
|
|
178
|
+
const res = await request(endpoint, props.form.data);
|
|
190
179
|
|
|
191
180
|
if (res.ok === false) {
|
|
192
|
-
const body = (await res.json()) as FormErrorBody<API, typeof path, typeof method>;
|
|
181
|
+
const body = (await res.json()) as FormErrorBody<API, typeof endpoint.path, typeof endpoint.method>;
|
|
193
182
|
|
|
194
183
|
// TODO set some kind of overall form error if there is no field
|
|
195
184
|
if (!body.field) {
|
|
@@ -204,7 +193,7 @@ export function createFormFunction<API extends ApiEndpoints>(
|
|
|
204
193
|
} else {
|
|
205
194
|
setMessage(props.form, 'Success');
|
|
206
195
|
if (actions && actions.onSuccess) {
|
|
207
|
-
const body = (await res.json()) as ApiSuccessBody<API, typeof path, typeof method>;
|
|
196
|
+
const body = (await res.json()) as ApiSuccessBody<API, typeof endpoint.path, typeof endpoint.method>;
|
|
208
197
|
await actions.onSuccess({ ...props, body });
|
|
209
198
|
}
|
|
210
199
|
}
|
|
@@ -219,7 +208,11 @@ export function createFormFunction<API extends ApiEndpoints>(
|
|
|
219
208
|
*/
|
|
220
209
|
const bindGet = async () => {
|
|
221
210
|
const formData = get(form.form);
|
|
222
|
-
return (await safeParseAsync(schema, bind.get(formData))).output as ValidInput<
|
|
211
|
+
return (await safeParseAsync(schema, bind.get(formData))).output as ValidInput<
|
|
212
|
+
API,
|
|
213
|
+
typeof endpoint.path,
|
|
214
|
+
typeof endpoint.method
|
|
215
|
+
>;
|
|
223
216
|
};
|
|
224
217
|
|
|
225
218
|
form.form.subscribe((v) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SvelteMap } from 'svelte/reactivity';
|
|
2
|
-
import
|
|
2
|
+
import { endpointKey } from 'ts-ag';
|
|
3
|
+
import type { ApiEndpointContract, ApiEndpoints, ApiRequestFunction, ApiInput, ApiResponse } from 'ts-ag';
|
|
3
4
|
|
|
4
5
|
import { Cache } from './cache.svelte.js';
|
|
5
6
|
import { Requestor, Query } from './query.svelte.js';
|
|
@@ -29,10 +30,24 @@ export type BatchDetails<
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
export type ApiBatchDetails<API extends ApiEndpoints> = {
|
|
32
|
-
[Path in API['path']]
|
|
33
|
-
[Method in Extract<API, { path: Path }>['method']]
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
[Path in API['path']]: {
|
|
34
|
+
[Method in Extract<API, { path: Path }>['method']]: {
|
|
35
|
+
endpoint: ApiEndpointContract<API, Path, Method>;
|
|
36
|
+
details: BatchDetails<API, Path, Method>;
|
|
37
|
+
};
|
|
38
|
+
}[Extract<API, { path: Path }>['method']];
|
|
39
|
+
}[API['path']][];
|
|
40
|
+
|
|
41
|
+
export function batchEndpoint<
|
|
42
|
+
API extends ApiEndpoints,
|
|
43
|
+
Path extends API['path'],
|
|
44
|
+
Method extends Extract<API, { path: Path }>['method']
|
|
45
|
+
>(
|
|
46
|
+
endpoint: ApiEndpointContract<API, Path, Method>,
|
|
47
|
+
details: BatchDetails<API, Path, Method>
|
|
48
|
+
): ApiBatchDetails<API>[number] {
|
|
49
|
+
return { endpoint, details };
|
|
50
|
+
}
|
|
36
51
|
|
|
37
52
|
/**
|
|
38
53
|
* Helper function to use once so that creating queries is easier.
|
|
@@ -45,21 +60,23 @@ export function createQueryFunction<API extends ApiEndpoints>(
|
|
|
45
60
|
request: ApiRequestFunction<API>,
|
|
46
61
|
batchDetails: ApiBatchDetails<API>
|
|
47
62
|
) {
|
|
63
|
+
// eslint-disable-next-line
|
|
64
|
+
const batchDetailsByEndpoint = new Map(batchDetails.map((entry) => [endpointKey(entry.endpoint), entry.details]));
|
|
65
|
+
|
|
48
66
|
return <Path extends API['path'], Method extends Extract<API, { path: Path }>['method']>(
|
|
49
|
-
|
|
50
|
-
method: Method,
|
|
67
|
+
endpoint: ApiEndpointContract<API, Path, Method>,
|
|
51
68
|
input: ApiInput<API, Path, Method>,
|
|
52
69
|
opts?: ConstructorParameters<typeof Query<API, Path, Method>>[0]['opts']
|
|
53
70
|
): Query<API, Path, Method> => {
|
|
54
|
-
const queryKey = cacheKey(
|
|
71
|
+
const queryKey = cacheKey(endpoint, input);
|
|
55
72
|
if (!queries.has(queryKey)) {
|
|
56
|
-
const key = batchQueryKey(
|
|
73
|
+
const key = batchQueryKey(endpoint);
|
|
57
74
|
if (!requestors.has(key)) {
|
|
58
|
-
requestors.set(key, new Requestor(
|
|
75
|
+
requestors.set(key, new Requestor(endpoint, request, cache, batchDetailsByEndpoint.get(key)));
|
|
59
76
|
}
|
|
60
77
|
const requestor = requestors.get(key)!;
|
|
61
78
|
|
|
62
|
-
queries.set(queryKey, new Query<API, Path, Method>({
|
|
79
|
+
queries.set(queryKey, new Query<API, Path, Method>({ endpoint, input, requestor, cache, opts }));
|
|
63
80
|
}
|
|
64
81
|
const query = queries.get(queryKey)!;
|
|
65
82
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createApiRequest, type ApiEndpoints } from 'ts-ag';
|
|
1
|
+
import { createApiRequest, type ApiEndpointContract, type ApiEndpoints } from 'ts-ag';
|
|
2
2
|
import * as v from 'valibot';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
@@ -17,11 +17,11 @@ type UsersApi = {
|
|
|
17
17
|
|
|
18
18
|
const API_URL = 'https://api.example.test';
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
'/users'
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
}
|
|
20
|
+
const users_POST = {
|
|
21
|
+
path: '/users',
|
|
22
|
+
method: 'POST',
|
|
23
|
+
schema: v.union([v.object({ id: v.number(), group: v.optional(v.string()) }), v.object({ ids: v.array(v.number()) })])
|
|
24
|
+
} satisfies ApiEndpointContract<UsersApi, '/users', 'POST'>;
|
|
25
25
|
|
|
26
26
|
function getUserId(input: UsersApi['requestInput']): number {
|
|
27
27
|
return 'id' in input ? input.id : input.ids[0]!;
|
|
@@ -49,39 +49,37 @@ describe('createQueryFunction', () => {
|
|
|
49
49
|
|
|
50
50
|
it('returns the same query instance for the same path, method, and input', async () => {
|
|
51
51
|
const { createQueryFunction } = await import('./entrypoint.svelte.js');
|
|
52
|
-
const request = createApiRequest<UsersApi>(
|
|
53
|
-
const createQuery = createQueryFunction<UsersApi>(request,
|
|
52
|
+
const request = createApiRequest<UsersApi>(API_URL, 'test');
|
|
53
|
+
const createQuery = createQueryFunction<UsersApi>(request, []);
|
|
54
54
|
|
|
55
|
-
const query1 = createQuery(
|
|
56
|
-
const query2 = createQuery(
|
|
57
|
-
const query3 = createQuery(
|
|
55
|
+
const query1 = createQuery(users_POST, { id: 1 });
|
|
56
|
+
const query2 = createQuery(users_POST, { id: 1 });
|
|
57
|
+
const query3 = createQuery(users_POST, { id: 2 });
|
|
58
58
|
|
|
59
59
|
expect(query1).toBe(query2);
|
|
60
60
|
expect(query3).not.toBe(query1);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
it('reuses requestors so separate queries can batch together', async () => {
|
|
64
|
-
const { createQueryFunction } = await import('./entrypoint.svelte.js');
|
|
64
|
+
const { batchEndpoint, createQueryFunction } = await import('./entrypoint.svelte.js');
|
|
65
65
|
const fetchMock = vi.fn(async () => jsonFetchResponse({ 1: 'one', 2: 'two' }));
|
|
66
66
|
vi.stubGlobal('fetch', fetchMock);
|
|
67
67
|
vi.stubGlobal('fetch', fetchMock);
|
|
68
|
-
const request = createApiRequest<UsersApi>(
|
|
69
|
-
const createQuery = createQueryFunction<UsersApi>(request,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
});
|
|
78
|
-
}
|
|
68
|
+
const request = createApiRequest<UsersApi>(API_URL, 'test');
|
|
69
|
+
const createQuery = createQueryFunction<UsersApi>(request, [
|
|
70
|
+
batchEndpoint(users_POST, {
|
|
71
|
+
canBatch: () => 'users',
|
|
72
|
+
batchInput: (inputs) => ({ ids: inputs.map(getUserId) }),
|
|
73
|
+
unBatchOutput: async (inputs, outputs) => {
|
|
74
|
+
return inputs.map(() => {
|
|
75
|
+
return outputs;
|
|
76
|
+
});
|
|
79
77
|
}
|
|
80
|
-
}
|
|
81
|
-
|
|
78
|
+
})
|
|
79
|
+
]);
|
|
82
80
|
|
|
83
|
-
const query1 = createQuery(
|
|
84
|
-
const query2 = createQuery(
|
|
81
|
+
const query1 = createQuery(users_POST, { id: 1 });
|
|
82
|
+
const query2 = createQuery(users_POST, { id: 2 });
|
|
85
83
|
|
|
86
84
|
const p1 = query1.request();
|
|
87
85
|
const p2 = query2.request();
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { stringify } from 'devalue';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
ApiEndpointContract,
|
|
4
|
+
ApiEndpoints,
|
|
5
|
+
ApiInput,
|
|
6
|
+
ApiRequestFunction,
|
|
7
|
+
ApiSuccessBody,
|
|
8
|
+
ApiErrorBody,
|
|
9
|
+
ApiResponse
|
|
10
|
+
} from 'ts-ag';
|
|
3
11
|
|
|
4
12
|
import type { Cache } from './cache.svelte';
|
|
5
13
|
import type { BatchDetails } from './entrypoint.svelte';
|
|
@@ -17,8 +25,7 @@ export class Query<
|
|
|
17
25
|
#TIMEOUT = 1000 * 60 * 5; // 5 minutes
|
|
18
26
|
|
|
19
27
|
// -------- Set in constructor --------
|
|
20
|
-
#
|
|
21
|
-
#method: Method;
|
|
28
|
+
#endpoint: ApiEndpointContract<API, Path, Method>;
|
|
22
29
|
#input: ApiInput<API, Path, Method>;
|
|
23
30
|
#inputString: string;
|
|
24
31
|
#cacheKey: string;
|
|
@@ -37,15 +44,13 @@ export class Query<
|
|
|
37
44
|
|
|
38
45
|
// -------- Functions --------
|
|
39
46
|
constructor({
|
|
40
|
-
|
|
41
|
-
method,
|
|
47
|
+
endpoint,
|
|
42
48
|
input,
|
|
43
49
|
requestor,
|
|
44
50
|
cache,
|
|
45
51
|
opts
|
|
46
52
|
}: {
|
|
47
|
-
|
|
48
|
-
method: Method;
|
|
53
|
+
endpoint: ApiEndpointContract<API, Path, Method>;
|
|
49
54
|
input: ApiInput<API, Path, Method>;
|
|
50
55
|
requestor: Requestor<API, Path, Method>;
|
|
51
56
|
cache: Cache;
|
|
@@ -56,14 +61,13 @@ export class Query<
|
|
|
56
61
|
this.#requestor = requestor;
|
|
57
62
|
this.#cache = cache;
|
|
58
63
|
|
|
59
|
-
this.#
|
|
60
|
-
this.#method = method;
|
|
64
|
+
this.#endpoint = endpoint;
|
|
61
65
|
|
|
62
66
|
// if (this.#cachekey) this.#cache.deregister(this.#cachekey);
|
|
63
67
|
|
|
64
68
|
this.#input = input;
|
|
65
69
|
this.#inputString = stringify(input);
|
|
66
|
-
this.#cacheKey = cacheKey(
|
|
70
|
+
this.#cacheKey = cacheKey(endpoint, input);
|
|
67
71
|
|
|
68
72
|
this.#cache.register(this.#cacheKey, opts?.cache ?? { timeout: this.#TIMEOUT });
|
|
69
73
|
}
|
|
@@ -141,8 +145,7 @@ export class Requestor<
|
|
|
141
145
|
#batchDelay = 100;
|
|
142
146
|
|
|
143
147
|
// -------- Set in constructor --------
|
|
144
|
-
#
|
|
145
|
-
#method: Method;
|
|
148
|
+
#endpoint: ApiEndpointContract<API, Path, Method>;
|
|
146
149
|
#request: ApiRequestFunction<API>;
|
|
147
150
|
|
|
148
151
|
#canBatch: BatchDetails<API, Path, Method>['canBatch'];
|
|
@@ -164,14 +167,12 @@ export class Requestor<
|
|
|
164
167
|
#batchTimers: Record<string, NodeJS.Timeout | null> = {};
|
|
165
168
|
|
|
166
169
|
constructor(
|
|
167
|
-
|
|
168
|
-
method: Method,
|
|
170
|
+
endpoint: ApiEndpointContract<API, Path, Method>,
|
|
169
171
|
request: ApiRequestFunction<API>,
|
|
170
172
|
_cache: Cache,
|
|
171
173
|
batchDetails?: BatchDetails<API, Path, Method>
|
|
172
174
|
) {
|
|
173
|
-
this.#
|
|
174
|
-
this.#method = method;
|
|
175
|
+
this.#endpoint = endpoint;
|
|
175
176
|
this.#request = request;
|
|
176
177
|
this.#limiter = new RateLimiter();
|
|
177
178
|
// this.#cache = cache;
|
|
@@ -187,7 +188,7 @@ export class Requestor<
|
|
|
187
188
|
// if ('PUBLIC_ENVIRONMENT' in env && env.PUBLIC_ENVIRONMENT === 'development') {
|
|
188
189
|
// await sleep(1000);
|
|
189
190
|
// }
|
|
190
|
-
return await this.#limiter.add(() => this.#request(this.#
|
|
191
|
+
return await this.#limiter.add(() => this.#request(this.#endpoint, input));
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stringify } from 'devalue';
|
|
2
|
-
import { createApiRequest, type ApiEndpoints } from 'ts-ag';
|
|
2
|
+
import { createApiRequest, type ApiEndpointContract, type ApiEndpoints } from 'ts-ag';
|
|
3
3
|
import * as v from 'valibot';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
@@ -30,17 +30,17 @@ type BatchedUsersApi = {
|
|
|
30
30
|
|
|
31
31
|
const API_URL = 'https://api.example.test';
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
'/users'
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
}
|
|
33
|
+
const plainUsers_GET = {
|
|
34
|
+
path: '/users',
|
|
35
|
+
method: 'GET',
|
|
36
|
+
schema: v.object({ id: v.number() })
|
|
37
|
+
} satisfies ApiEndpointContract<PlainUsersApi, '/users', 'GET'>;
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
'/users'
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
39
|
+
const batchedUsers_POST = {
|
|
40
|
+
path: '/users',
|
|
41
|
+
method: 'POST',
|
|
42
|
+
schema: v.union([v.object({ id: v.number(), group: v.optional(v.string()) }), v.object({ ids: v.array(v.number()) })])
|
|
43
|
+
} satisfies ApiEndpointContract<BatchedUsersApi, '/users', 'POST'>;
|
|
44
44
|
|
|
45
45
|
function getSingleId(input: BatchedUsersApi['requestInput']): number {
|
|
46
46
|
return 'id' in input ? input.id : input.ids[0]!;
|
|
@@ -97,23 +97,22 @@ function deferred<T>() {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function createPlainRequest() {
|
|
100
|
-
return createApiRequest<PlainUsersApi>(
|
|
100
|
+
return createApiRequest<PlainUsersApi>(API_URL, 'test');
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function createBatchedRequest() {
|
|
104
|
-
return createApiRequest<BatchedUsersApi>(
|
|
104
|
+
return createApiRequest<BatchedUsersApi>(API_URL, 'test');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function createPlainRequestor() {
|
|
108
|
-
return new Requestor<PlainUsersApi, '/users', 'GET'>(
|
|
108
|
+
return new Requestor<PlainUsersApi, '/users', 'GET'>(plainUsers_GET, createPlainRequest(), new Cache());
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
function createBatchedRequestor(
|
|
112
|
-
batchDetails?: ConstructorParameters<typeof Requestor<BatchedUsersApi, '/users', 'POST'>>[
|
|
112
|
+
batchDetails?: ConstructorParameters<typeof Requestor<BatchedUsersApi, '/users', 'POST'>>[3]
|
|
113
113
|
) {
|
|
114
114
|
return new Requestor<BatchedUsersApi, '/users', 'POST'>(
|
|
115
|
-
|
|
116
|
-
'POST',
|
|
115
|
+
batchedUsers_POST,
|
|
117
116
|
createBatchedRequest(),
|
|
118
117
|
new Cache(),
|
|
119
118
|
batchDetails
|
|
@@ -355,8 +354,7 @@ describe('Query', () => {
|
|
|
355
354
|
const fetchMock = vi.fn().mockReturnValue(pending.promise);
|
|
356
355
|
vi.stubGlobal('fetch', fetchMock);
|
|
357
356
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
358
|
-
|
|
359
|
-
method: 'GET',
|
|
357
|
+
endpoint: plainUsers_GET,
|
|
360
358
|
input: { id: 1 },
|
|
361
359
|
requestor: createPlainRequestor(),
|
|
362
360
|
cache: new Cache()
|
|
@@ -378,8 +376,7 @@ describe('Query', () => {
|
|
|
378
376
|
const fetchMock = vi.fn(async () => jsonFetchResponse({ id: 1, name: 'Ada' }));
|
|
379
377
|
vi.stubGlobal('fetch', fetchMock);
|
|
380
378
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
381
|
-
|
|
382
|
-
method: 'GET',
|
|
379
|
+
endpoint: plainUsers_GET,
|
|
383
380
|
input: { id: 1 },
|
|
384
381
|
requestor: createPlainRequestor(),
|
|
385
382
|
cache: new Cache()
|
|
@@ -400,8 +397,7 @@ describe('Query', () => {
|
|
|
400
397
|
);
|
|
401
398
|
vi.stubGlobal('fetch', fetchMock);
|
|
402
399
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
403
|
-
|
|
404
|
-
method: 'GET',
|
|
400
|
+
endpoint: plainUsers_GET,
|
|
405
401
|
input: { id: 1 },
|
|
406
402
|
requestor: createPlainRequestor(),
|
|
407
403
|
cache: new Cache()
|
|
@@ -430,8 +426,7 @@ describe('Query', () => {
|
|
|
430
426
|
const fetchMock = vi.fn(async () => withResponseOverrides(jsonFetchResponse({ id: 1 })));
|
|
431
427
|
vi.stubGlobal('fetch', fetchMock);
|
|
432
428
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
433
|
-
|
|
434
|
-
method: 'GET',
|
|
429
|
+
endpoint: plainUsers_GET,
|
|
435
430
|
input: { id: 1 },
|
|
436
431
|
requestor: createPlainRequestor(),
|
|
437
432
|
cache: new Cache()
|
|
@@ -457,8 +452,7 @@ describe('Query', () => {
|
|
|
457
452
|
const fetchMock = vi.fn(async () => jsonFetchResponse({ id: 1, active: true }));
|
|
458
453
|
vi.stubGlobal('fetch', fetchMock);
|
|
459
454
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
460
|
-
|
|
461
|
-
method: 'GET',
|
|
455
|
+
endpoint: plainUsers_GET,
|
|
462
456
|
input: { id: 1 },
|
|
463
457
|
requestor: createPlainRequestor(),
|
|
464
458
|
cache: new Cache()
|
|
@@ -476,8 +470,7 @@ describe('Query', () => {
|
|
|
476
470
|
const fetchMock = vi.fn(async () => jsonFetchResponse({ message: 'missing' }, 404));
|
|
477
471
|
vi.stubGlobal('fetch', fetchMock);
|
|
478
472
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
479
|
-
|
|
480
|
-
method: 'GET',
|
|
473
|
+
endpoint: plainUsers_GET,
|
|
481
474
|
input: { id: 99 },
|
|
482
475
|
requestor: createPlainRequestor(),
|
|
483
476
|
cache: new Cache()
|
|
@@ -505,8 +498,7 @@ describe('Query', () => {
|
|
|
505
498
|
});
|
|
506
499
|
vi.stubGlobal('fetch', fetchMock);
|
|
507
500
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
508
|
-
|
|
509
|
-
method: 'GET',
|
|
501
|
+
endpoint: plainUsers_GET,
|
|
510
502
|
input: { id: 1 },
|
|
511
503
|
requestor: createPlainRequestor(),
|
|
512
504
|
cache: new Cache()
|
|
@@ -532,8 +524,7 @@ describe('Query', () => {
|
|
|
532
524
|
});
|
|
533
525
|
vi.stubGlobal('fetch', fetchMock);
|
|
534
526
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
535
|
-
|
|
536
|
-
method: 'GET',
|
|
527
|
+
endpoint: plainUsers_GET,
|
|
537
528
|
input: { id: 1 },
|
|
538
529
|
requestor: createPlainRequestor(),
|
|
539
530
|
cache: new Cache()
|
|
@@ -558,8 +549,7 @@ describe('Query', () => {
|
|
|
558
549
|
});
|
|
559
550
|
vi.stubGlobal('fetch', fetchMock);
|
|
560
551
|
const query = new Query<PlainUsersApi, '/users', 'GET'>({
|
|
561
|
-
|
|
562
|
-
method: 'GET',
|
|
552
|
+
endpoint: plainUsers_GET,
|
|
563
553
|
input: { id: 1 },
|
|
564
554
|
requestor: createPlainRequestor(),
|
|
565
555
|
cache: new Cache(),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { stringify } from 'devalue';
|
|
2
|
+
import { endpointKey } from 'ts-ag';
|
|
2
3
|
|
|
3
|
-
export function batchQueryKey(path: string
|
|
4
|
-
return
|
|
4
|
+
export function batchQueryKey(endpoint: { path: string; method: string }) {
|
|
5
|
+
return endpointKey(endpoint);
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
export function cacheKey(path: string
|
|
8
|
-
return `${
|
|
8
|
+
export function cacheKey(endpoint: { path: string; method: string }, input: any) {
|
|
9
|
+
return `${endpointKey(endpoint)} ${stringify(input)}`;
|
|
9
10
|
}
|