@laguzjs/client 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +30 -2
- package/dist/http.d.ts +9 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +36 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/pool.d.ts +52 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +339 -0
- package/dist/pool.js.map +1 -0
- package/dist/react.d.ts +45 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +127 -0
- package/dist/react.js.map +1 -0
- package/dist/selection.d.ts +68 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +161 -0
- package/dist/selection.js.map +1 -0
- package/package.json +59 -4
- package/src/http.ts +42 -0
- package/src/index.ts +6 -0
- package/src/pool.ts +441 -0
- package/src/react.ts +151 -0
- package/src/selection.ts +187 -0
package/src/selection.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a consumer selects on a root field.
|
|
3
|
+
*
|
|
4
|
+
* - `"name"`: a scalar field
|
|
5
|
+
* - `{ projects: ["id"] }`: a nested selection
|
|
6
|
+
* - `{ projects: { args: { first: 3 }, fields: ["id"] } }`: a field with arguments
|
|
7
|
+
* - `{ recent: { field: "projects", args: { first: 3 }, fields: ["id"] } }`: the same field under another name
|
|
8
|
+
*/
|
|
9
|
+
export type Fields = (string | { [out: string]: Fields | FieldArg })[];
|
|
10
|
+
export type FieldArg = { field?: string; args?: Record<string, unknown>; fields?: Fields };
|
|
11
|
+
|
|
12
|
+
/** A merged selection. Keys are `field` or `field(args)`; a leaf is `{}`. */
|
|
13
|
+
export type Tree = { [key: string]: Tree };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An argument value read from another root field's result, for queries that
|
|
17
|
+
* depend on each other. See {@link ref}.
|
|
18
|
+
*/
|
|
19
|
+
export type Ref = { $ref: [root: string, args: object, path: string] };
|
|
20
|
+
|
|
21
|
+
const NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
22
|
+
|
|
23
|
+
export function name(s: string): string {
|
|
24
|
+
if (!NAME.test(s)) throw new Error(`invalid GraphQL name: ${s}`);
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Use the value at `path` in `root(args)` as an argument:
|
|
30
|
+
* `ref("user", { id: 1 }, "teamId")`. Refs can be nested.
|
|
31
|
+
*/
|
|
32
|
+
export function ref(root: string, args: object, path: string): Ref {
|
|
33
|
+
for (const segment of path.split(".")) {
|
|
34
|
+
if (name(segment).includes("__"))
|
|
35
|
+
throw new Error(`ref path segments can't contain "__": ${segment}`);
|
|
36
|
+
}
|
|
37
|
+
return { $ref: [name(root), args, path] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const isRef = (v: unknown): v is Ref => v !== null && typeof v === "object" && "$ref" in v;
|
|
41
|
+
|
|
42
|
+
export function collectRefs(v: unknown): Ref[] {
|
|
43
|
+
if (isRef(v)) return [v];
|
|
44
|
+
if (v && typeof v === "object") return Object.values(v).flatMap(collectRefs);
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const pathTree = (path: string): Tree =>
|
|
49
|
+
path.split(".").reduceRight((tree: Tree, key) => ({ [key]: tree }), {});
|
|
50
|
+
|
|
51
|
+
export const getPath = (data: any, path: string): unknown =>
|
|
52
|
+
path.split(".").reduce((value, key) => value?.[key], data);
|
|
53
|
+
|
|
54
|
+
export function toTree(fields: Fields): Tree {
|
|
55
|
+
const tree: Tree = {};
|
|
56
|
+
for (const f of fields) {
|
|
57
|
+
if (typeof f === "string") {
|
|
58
|
+
tree[name(f)] ??= {};
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
for (const [out, v] of Object.entries(f)) {
|
|
62
|
+
name(out);
|
|
63
|
+
const key = Array.isArray(v) ? out : canonical(v.field ?? out, v.args ?? {});
|
|
64
|
+
const sub = Array.isArray(v) ? v : (v.fields ?? []);
|
|
65
|
+
tree[key] = merge(tree[key] ?? {}, toTree(sub));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return tree;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function merge(a: Tree, b: Tree): Tree {
|
|
72
|
+
const tree = { ...a };
|
|
73
|
+
for (const key in b) tree[key] = merge(tree[key] ?? {}, b[key]!);
|
|
74
|
+
return tree;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The part of `want` that `have` doesn't cover, or null. */
|
|
78
|
+
export function diff(want: Tree, have: Tree): Tree | null {
|
|
79
|
+
let missing: Tree | null = null;
|
|
80
|
+
for (const key in want) {
|
|
81
|
+
const sub = have[key] ? diff(want[key]!, have[key]) : want[key]!;
|
|
82
|
+
if (sub) (missing ??= {})[key] = sub;
|
|
83
|
+
}
|
|
84
|
+
return missing;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* True when fetching `tree` would add fields inside a list we already hold.
|
|
89
|
+
* Merging two fetches of a list by index is wrong if the list changed in
|
|
90
|
+
* between, so the caller refetches the whole selection instead.
|
|
91
|
+
*/
|
|
92
|
+
export function touchesList(tree: Tree, data: any): boolean {
|
|
93
|
+
if (!data || typeof data !== "object") return false;
|
|
94
|
+
if (Array.isArray(data)) return true;
|
|
95
|
+
return Object.keys(tree).some((key) => touchesList(tree[key]!, data[responseKey(key)]));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const canonical = (field: string, args: object) => name(field) + printArgs(args);
|
|
99
|
+
|
|
100
|
+
// FNV-1a in base 36: a short, stable alias suffix.
|
|
101
|
+
function hash(s: string): string {
|
|
102
|
+
let h = 2166136261;
|
|
103
|
+
for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619);
|
|
104
|
+
return (h >>> 0).toString(36);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Fields with arguments are aliased: two consumers may select the same field
|
|
108
|
+
// with different arguments, and both results have to coexist in one entry.
|
|
109
|
+
function responseKey(key: string): string {
|
|
110
|
+
const paren = key.indexOf("(");
|
|
111
|
+
return paren < 0 ? key : `${key.slice(0, paren)}_${hash(key)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The key under which `field(args)` is stored in raw entry data. */
|
|
115
|
+
export const fieldKey = (field: string, args: object = {}): string =>
|
|
116
|
+
responseKey(canonical(field, args));
|
|
117
|
+
|
|
118
|
+
/** Project raw entry data onto one consumer's selection, with its own output names. */
|
|
119
|
+
export function view(data: any, fields: Fields): any {
|
|
120
|
+
if (data == null || typeof data !== "object") return data;
|
|
121
|
+
if (Array.isArray(data)) return data.map((item) => view(item, fields));
|
|
122
|
+
const out: any = {};
|
|
123
|
+
for (const f of fields) {
|
|
124
|
+
if (typeof f === "string") {
|
|
125
|
+
out[f] = data[f];
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const [key, v] of Object.entries(f)) {
|
|
129
|
+
if (Array.isArray(v)) {
|
|
130
|
+
out[key] = view(data[key], v);
|
|
131
|
+
} else {
|
|
132
|
+
const raw = data[fieldKey(v.field ?? key, v.args)];
|
|
133
|
+
out[key] = v.fields ? view(raw, v.fields) : raw;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function printTree(tree: Tree): string {
|
|
141
|
+
const keys = Object.keys(tree);
|
|
142
|
+
if (!keys.length) return "";
|
|
143
|
+
const field = (key: string) => (responseKey(key) === key ? key : `${responseKey(key)}: ${key}`);
|
|
144
|
+
return ` { ${keys.map((key) => field(key) + printTree(tree[key]!)).join(" ")} }`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Print a JS value as a GraphQL literal. JSON string escaping is valid GraphQL
|
|
149
|
+
* string escaping. Object keys are sorted so equal arguments print the same.
|
|
150
|
+
* Refs print through `onRef`, or as a readable key when it is absent.
|
|
151
|
+
* Enum values are not supported yet: strings always print quoted.
|
|
152
|
+
*/
|
|
153
|
+
export function printValue(v: any, onRef?: (r: Ref) => string): string {
|
|
154
|
+
if (isRef(v)) return onRef ? onRef(v) : `@${v.$ref[0]}${printArgs(v.$ref[1])}.${v.$ref[2]}`;
|
|
155
|
+
if (typeof v?.toJSON === "function") v = v.toJSON();
|
|
156
|
+
if (Array.isArray(v)) return `[${v.map((x) => printValue(x, onRef)).join(", ")}]`;
|
|
157
|
+
if (v && typeof v === "object") {
|
|
158
|
+
const keys = Object.keys(v)
|
|
159
|
+
.filter((key) => v[key] !== undefined)
|
|
160
|
+
.sort();
|
|
161
|
+
return `{${keys.map((key) => `${name(key)}: ${printValue(v[key], onRef)}`).join(", ")}}`;
|
|
162
|
+
}
|
|
163
|
+
return JSON.stringify(v ?? null);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function printArgs(args: object, onRef?: (r: Ref) => string): string {
|
|
167
|
+
const inner = printValue(args, onRef).slice(1, -1);
|
|
168
|
+
return inner ? `(${inner})` : "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `root(args) { fields }`: the text the pool sends for a single consumer. */
|
|
172
|
+
export const printField = (root: string, args: object, fields: Fields): string =>
|
|
173
|
+
name(root) + printArgs(args) + printTree(toTree(fields));
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Merge a partial result into cached data. Two arrays only meet here when they
|
|
177
|
+
* come from the same fetch shape, because `touchesList` forces whole refetches.
|
|
178
|
+
*/
|
|
179
|
+
export function mergeData(a: any, b: any): any {
|
|
180
|
+
if (Array.isArray(a) && Array.isArray(b)) return b.map((item, i) => mergeData(a[i], item));
|
|
181
|
+
if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(b)) {
|
|
182
|
+
const out = { ...a };
|
|
183
|
+
for (const key in b) out[key] = mergeData(a[key], b[key]);
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
return b;
|
|
187
|
+
}
|