@zenstackhq/client-helpers 3.1.0 → 3.2.0
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/package.json +12 -9
- package/.turbo/turbo-build.log +0 -24
- package/eslint.config.js +0 -4
- package/src/constants.ts +0 -4
- package/src/fetch.ts +0 -107
- package/src/index.ts +0 -9
- package/src/invalidation.ts +0 -89
- package/src/logging.ts +0 -15
- package/src/mutator.ts +0 -449
- package/src/nested-read-visitor.ts +0 -68
- package/src/nested-write-visitor.ts +0 -359
- package/src/optimistic.ts +0 -139
- package/src/query-analysis.ts +0 -111
- package/src/types.ts +0 -82
- package/test/fetch.test.ts +0 -423
- package/test/invalidation.test.ts +0 -602
- package/test/mutator.test.ts +0 -1533
- package/test/nested-read-visitor.test.ts +0 -949
- package/test/nested-write-visitor.test.ts +0 -1244
- package/test/optimistic.test.ts +0 -743
- package/test/query-analysis.test.ts +0 -1399
- package/test/test-helpers.ts +0 -37
- package/tsconfig.json +0 -4
- package/tsconfig.test.json +0 -7
- package/tsup.config.ts +0 -14
- package/vitest.config.ts +0 -4
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenstackhq/client-helpers",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Helpers for implementing clients that consume ZenStack's CRUD service",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "ZenStack Team",
|
|
7
7
|
"license": "MIT",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
8
11
|
"exports": {
|
|
9
12
|
".": {
|
|
10
13
|
"types": "./dist/index.d.ts",
|
|
@@ -18,16 +21,16 @@
|
|
|
18
21
|
"dependencies": {
|
|
19
22
|
"decimal.js": "^10.4.3",
|
|
20
23
|
"superjson": "^2.2.3",
|
|
21
|
-
"@zenstackhq/
|
|
22
|
-
"@zenstackhq/
|
|
24
|
+
"@zenstackhq/schema": "3.2.0",
|
|
25
|
+
"@zenstackhq/common-helpers": "3.2.0"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
|
-
"@zenstackhq/eslint-config": "3.
|
|
26
|
-
"@zenstackhq/language": "3.
|
|
27
|
-
"@zenstackhq/
|
|
28
|
-
"@zenstackhq/
|
|
29
|
-
"@zenstackhq/
|
|
30
|
-
"@zenstackhq/
|
|
28
|
+
"@zenstackhq/eslint-config": "3.2.0",
|
|
29
|
+
"@zenstackhq/language": "3.2.0",
|
|
30
|
+
"@zenstackhq/orm": "3.2.0",
|
|
31
|
+
"@zenstackhq/sdk": "3.2.0",
|
|
32
|
+
"@zenstackhq/vitest-config": "3.2.0",
|
|
33
|
+
"@zenstackhq/typescript-config": "3.2.0"
|
|
31
34
|
},
|
|
32
35
|
"scripts": {
|
|
33
36
|
"build": "tsc --noEmit && tsup-node && pnpm test:typecheck",
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @zenstackhq/client-helpers@3.1.0 build /home/runner/work/zenstack-v3/zenstack-v3/packages/clients/client-helpers
|
|
3
|
-
> tsc --noEmit && tsup-node && pnpm test:typecheck
|
|
4
|
-
|
|
5
|
-
[34mCLI[39m Building entry: {"index":"src/index.ts","fetch":"src/fetch.ts"}
|
|
6
|
-
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
7
|
-
[34mCLI[39m tsup v8.5.0
|
|
8
|
-
[34mCLI[39m Using tsup config: /home/runner/work/zenstack-v3/zenstack-v3/packages/clients/client-helpers/tsup.config.ts
|
|
9
|
-
[34mCLI[39m Target: esnext
|
|
10
|
-
[34mCLI[39m Cleaning output folder
|
|
11
|
-
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[32m25.68 KB[39m
|
|
13
|
-
[32mESM[39m [1mdist/fetch.js [22m[32m2.73 KB[39m
|
|
14
|
-
[32mESM[39m [1mdist/index.js.map [22m[32m63.32 KB[39m
|
|
15
|
-
[32mESM[39m [1mdist/fetch.js.map [22m[32m5.94 KB[39m
|
|
16
|
-
[32mESM[39m ⚡️ Build success in 136ms
|
|
17
|
-
[34mDTS[39m Build start
|
|
18
|
-
[32mDTS[39m ⚡️ Build success in 5075ms
|
|
19
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m10.28 KB[39m
|
|
20
|
-
[32mDTS[39m [1mdist/fetch.d.ts [22m[32m1.24 KB[39m
|
|
21
|
-
|
|
22
|
-
> @zenstackhq/client-helpers@3.1.0 test:typecheck /home/runner/work/zenstack-v3/zenstack-v3/packages/clients/client-helpers
|
|
23
|
-
> tsc --noEmit --project tsconfig.test.json
|
|
24
|
-
|
package/eslint.config.js
DELETED
package/src/constants.ts
DELETED
package/src/fetch.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { lowerCaseFirst } from '@zenstackhq/common-helpers';
|
|
2
|
-
import Decimal from 'decimal.js';
|
|
3
|
-
import SuperJSON from 'superjson';
|
|
4
|
-
import type { QueryError } from './types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Function signature for `fetch`.
|
|
8
|
-
*/
|
|
9
|
-
export type FetchFn = (url: string, options?: RequestInit) => Promise<Response>;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* A fetcher function that uses fetch API to make HTTP requests and automatically unmarshals
|
|
13
|
-
* the response using superjson.
|
|
14
|
-
*/
|
|
15
|
-
export async function fetcher<R>(url: string, options?: RequestInit, customFetch?: FetchFn): Promise<R> {
|
|
16
|
-
const _fetch = customFetch ?? fetch;
|
|
17
|
-
const res = await _fetch(url, options);
|
|
18
|
-
if (!res.ok) {
|
|
19
|
-
const errData = unmarshal(await res.text());
|
|
20
|
-
if (errData.error?.rejectedByPolicy && errData.error?.rejectReason === 'cannot-read-back') {
|
|
21
|
-
// policy doesn't allow mutation result to be read back, just return undefined
|
|
22
|
-
return undefined as any;
|
|
23
|
-
}
|
|
24
|
-
const error: QueryError = new Error('An error occurred while fetching the data.');
|
|
25
|
-
error.info = errData.error;
|
|
26
|
-
error.status = res.status;
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const textResult = await res.text();
|
|
31
|
-
try {
|
|
32
|
-
return unmarshal(textResult).data as R;
|
|
33
|
-
} catch (err) {
|
|
34
|
-
console.error(`Unable to deserialize data:`, textResult);
|
|
35
|
-
throw err;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Makes a URL for the given endpoint, model, operation, and args that matches the RPC-style server API.
|
|
41
|
-
*/
|
|
42
|
-
export function makeUrl(endpoint: string, model: string, operation: string, args?: unknown) {
|
|
43
|
-
const baseUrl = `${endpoint}/${lowerCaseFirst(model)}/${operation}`;
|
|
44
|
-
if (!args) {
|
|
45
|
-
return baseUrl;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const { data, meta } = serialize(args);
|
|
49
|
-
let result = `${baseUrl}?q=${encodeURIComponent(JSON.stringify(data))}`;
|
|
50
|
-
if (meta) {
|
|
51
|
-
result += `&meta=${encodeURIComponent(JSON.stringify({ serialization: meta }))}`;
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
SuperJSON.registerCustom<Decimal, string>(
|
|
57
|
-
{
|
|
58
|
-
isApplicable: (v): v is Decimal =>
|
|
59
|
-
v instanceof Decimal ||
|
|
60
|
-
// interop with decimal.js
|
|
61
|
-
v?.toStringTag === '[object Decimal]',
|
|
62
|
-
serialize: (v) => v.toJSON(),
|
|
63
|
-
deserialize: (v) => new Decimal(v),
|
|
64
|
-
},
|
|
65
|
-
'Decimal',
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Serialize the given value with superjson
|
|
70
|
-
*/
|
|
71
|
-
export function serialize(value: unknown): { data: unknown; meta: unknown } {
|
|
72
|
-
const { json, meta } = SuperJSON.serialize(value);
|
|
73
|
-
return { data: json, meta };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Deserialize the given value with superjson using the given metadata
|
|
78
|
-
*/
|
|
79
|
-
export function deserialize(value: unknown, meta: any): unknown {
|
|
80
|
-
return SuperJSON.deserialize({ json: value as any, meta });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Marshal the given value to a string using superjson
|
|
85
|
-
*/
|
|
86
|
-
export function marshal(value: unknown) {
|
|
87
|
-
const { data, meta } = serialize(value);
|
|
88
|
-
if (meta) {
|
|
89
|
-
return JSON.stringify({ ...(data as any), meta: { serialization: meta } });
|
|
90
|
-
} else {
|
|
91
|
-
return JSON.stringify(data);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Unmarshal the given string value using superjson, assuming the value is a JSON stringified
|
|
97
|
-
* object containing the serialized data and serialization metadata.
|
|
98
|
-
*/
|
|
99
|
-
export function unmarshal(value: string) {
|
|
100
|
-
const parsed = JSON.parse(value);
|
|
101
|
-
if (typeof parsed === 'object' && parsed?.data && parsed?.meta?.serialization) {
|
|
102
|
-
const deserializedData = deserialize(parsed.data, parsed.meta.serialization);
|
|
103
|
-
return { ...parsed, data: deserializedData };
|
|
104
|
-
} else {
|
|
105
|
-
return parsed;
|
|
106
|
-
}
|
|
107
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export * from './constants';
|
|
2
|
-
export * from './invalidation';
|
|
3
|
-
export * from './logging';
|
|
4
|
-
export * from './mutator';
|
|
5
|
-
export * from './nested-read-visitor';
|
|
6
|
-
export * from './nested-write-visitor';
|
|
7
|
-
export * from './optimistic';
|
|
8
|
-
export * from './query-analysis';
|
|
9
|
-
export * from './types';
|
package/src/invalidation.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { SchemaDef } from '@zenstackhq/schema';
|
|
2
|
-
import { log, type Logger } from './logging';
|
|
3
|
-
import { getMutatedModels, getReadModels } from './query-analysis';
|
|
4
|
-
import type { MaybePromise, ORMWriteActionType } from './types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Type for a predicate that determines whether a query should be invalidated.
|
|
8
|
-
*/
|
|
9
|
-
export type InvalidationPredicate = ({ model, args }: { model: string; args: unknown }) => boolean;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Type for a function that invalidates queries matching the given predicate.
|
|
13
|
-
*/
|
|
14
|
-
export type InvalidateFunc = (predicate: InvalidationPredicate) => MaybePromise<void>;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Create a function that invalidates queries affected by the given mutation operation.
|
|
18
|
-
*
|
|
19
|
-
* @param model Model under mutation.
|
|
20
|
-
* @param operation Mutation operation (e.g, `update`).
|
|
21
|
-
* @param schema The schema.
|
|
22
|
-
* @param invalidator Function to invalidate queries matching a predicate. It should internally
|
|
23
|
-
* enumerate all query cache entries and invalidate those for which the predicate returns true.
|
|
24
|
-
* @param logging Logging option.
|
|
25
|
-
*/
|
|
26
|
-
export function createInvalidator(
|
|
27
|
-
model: string,
|
|
28
|
-
operation: string,
|
|
29
|
-
schema: SchemaDef,
|
|
30
|
-
invalidator: InvalidateFunc,
|
|
31
|
-
logging: Logger | undefined,
|
|
32
|
-
) {
|
|
33
|
-
return async (...args: unknown[]) => {
|
|
34
|
-
const [_, variables] = args;
|
|
35
|
-
const predicate = await getInvalidationPredicate(
|
|
36
|
-
model,
|
|
37
|
-
operation as ORMWriteActionType,
|
|
38
|
-
variables,
|
|
39
|
-
schema,
|
|
40
|
-
logging,
|
|
41
|
-
);
|
|
42
|
-
await invalidator(predicate);
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// gets a predicate for evaluating whether a query should be invalidated
|
|
47
|
-
async function getInvalidationPredicate(
|
|
48
|
-
model: string,
|
|
49
|
-
operation: ORMWriteActionType,
|
|
50
|
-
mutationArgs: any,
|
|
51
|
-
schema: SchemaDef,
|
|
52
|
-
logging: Logger | undefined,
|
|
53
|
-
): Promise<InvalidationPredicate> {
|
|
54
|
-
const mutatedModels = await getMutatedModels(model, operation, mutationArgs, schema);
|
|
55
|
-
|
|
56
|
-
return ({ model, args }) => {
|
|
57
|
-
if (mutatedModels.includes(model)) {
|
|
58
|
-
// direct match
|
|
59
|
-
if (logging) {
|
|
60
|
-
log(
|
|
61
|
-
logging,
|
|
62
|
-
`Marking "${model}" query for invalidation due to mutation "${operation}", query args: ${JSON.stringify(args)}`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (args) {
|
|
69
|
-
// traverse query args to find nested reads that match the model under mutation
|
|
70
|
-
if (findNestedRead(model, mutatedModels, schema, args)) {
|
|
71
|
-
if (logging) {
|
|
72
|
-
log(
|
|
73
|
-
logging,
|
|
74
|
-
`Marking "${model}" query for invalidation due to mutation "${operation}", query args: ${JSON.stringify(args)}`,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return false;
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// find nested reads that match the given models
|
|
86
|
-
function findNestedRead(visitingModel: string, targetModels: string[], schema: SchemaDef, args: any) {
|
|
87
|
-
const modelsRead = getReadModels(visitingModel, schema, args);
|
|
88
|
-
return targetModels.some((m) => modelsRead.includes(m));
|
|
89
|
-
}
|
package/src/logging.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Logger configuration. `true` enables console logging. A function can be provided for custom logging.
|
|
3
|
-
*/
|
|
4
|
-
export type Logger = boolean | ((message: string) => void);
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Logs a message using the provided logger.
|
|
8
|
-
*/
|
|
9
|
-
export function log(logger: Logger, message: string) {
|
|
10
|
-
if (typeof logger === 'function') {
|
|
11
|
-
logger(message);
|
|
12
|
-
} else if (logger) {
|
|
13
|
-
console.log(message);
|
|
14
|
-
}
|
|
15
|
-
}
|