@lewebsimple/nuxt-graphql 0.2.2 → 0.3.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/README.md +361 -55
- package/dist/module.d.mts +33 -21
- package/dist/module.json +1 -1
- package/dist/module.mjs +337 -373
- package/dist/runtime/app/composables/useGraphQLCache.client.d.ts +10 -0
- package/dist/runtime/app/composables/useGraphQLCache.client.js +41 -0
- package/dist/runtime/app/composables/useGraphQLMutation.d.ts +17 -8
- package/dist/runtime/app/composables/useGraphQLMutation.js +10 -9
- package/dist/runtime/app/composables/useGraphQLQuery.d.ts +5 -12
- package/dist/runtime/app/composables/useGraphQLQuery.js +74 -28
- package/dist/runtime/app/composables/useGraphQLSubscription.d.ts +5 -6
- package/dist/runtime/app/composables/useGraphQLSubscription.js +13 -12
- package/dist/runtime/app/lib/graphql-cache.d.ts +7 -0
- package/dist/runtime/app/lib/graphql-cache.js +24 -0
- package/dist/runtime/app/lib/persisted.d.ts +4 -0
- package/dist/runtime/app/lib/persisted.js +70 -0
- package/dist/runtime/app/plugins/graphql-request.d.ts +7 -0
- package/dist/runtime/app/plugins/graphql-request.js +21 -0
- package/dist/runtime/app/plugins/graphql-sse.client.d.ts +7 -0
- package/dist/runtime/app/plugins/graphql-sse.client.js +15 -0
- package/dist/runtime/app/types/nuxt-graphql.d.ts +37 -0
- package/dist/runtime/server/api/yoga-handler.js +42 -0
- package/dist/runtime/server/lib/define-graphql-context.d.ts +5 -0
- package/dist/runtime/server/lib/define-graphql-context.js +4 -0
- package/dist/runtime/server/lib/define-remote-exec-middleware.d.ts +30 -0
- package/dist/runtime/server/lib/define-remote-exec-middleware.js +3 -0
- package/dist/runtime/server/lib/define-yoga-middleware.d.ts +21 -0
- package/dist/runtime/server/lib/define-yoga-middleware.js +3 -0
- package/dist/runtime/server/lib/execute-server-graphql.d.ts +7 -0
- package/dist/runtime/server/lib/execute-server-graphql.js +34 -0
- package/dist/runtime/server/lib/remote-executor.d.ts +35 -0
- package/dist/runtime/server/lib/remote-executor.js +64 -0
- package/dist/runtime/server/tsconfig.json +1 -1
- package/dist/runtime/server/utils/useServerGraphQLMutation.d.ts +8 -14
- package/dist/runtime/server/utils/useServerGraphQLMutation.js +8 -11
- package/dist/runtime/server/utils/useServerGraphQLQuery.d.ts +2 -10
- package/dist/runtime/server/utils/useServerGraphQLQuery.js +3 -4
- package/dist/runtime/shared/lib/graphql-error.d.ts +17 -0
- package/dist/runtime/shared/lib/graphql-error.js +28 -0
- package/dist/runtime/shared/lib/headers.d.ts +3 -0
- package/dist/runtime/shared/lib/headers.js +39 -0
- package/dist/types.d.mts +13 -1
- package/package.json +10 -14
- package/dist/runtime/app/composables/useGraphQLCache.d.ts +0 -10
- package/dist/runtime/app/composables/useGraphQLCache.js +0 -15
- package/dist/runtime/app/plugins/graphql.d.ts +0 -31
- package/dist/runtime/app/plugins/graphql.js +0 -42
- package/dist/runtime/app/utils/graphql-cache.d.ts +0 -36
- package/dist/runtime/app/utils/graphql-cache.js +0 -65
- package/dist/runtime/app/utils/graphql-error.d.ts +0 -12
- package/dist/runtime/app/utils/graphql-error.js +0 -24
- package/dist/runtime/server/api/graphql-handler.js +0 -15
- package/dist/runtime/server/lib/constants.d.ts +0 -1
- package/dist/runtime/server/lib/constants.js +0 -1
- package/dist/runtime/server/lib/create-yoga.d.ts +0 -1
- package/dist/runtime/server/lib/create-yoga.js +0 -17
- package/dist/runtime/server/lib/default-context.d.ts +0 -7
- package/dist/runtime/server/lib/default-context.js +0 -1
- package/dist/runtime/server/utils/graphql-client.d.ts +0 -14
- package/dist/runtime/server/utils/graphql-client.js +0 -14
- package/dist/runtime/server/utils/remote-middleware.d.ts +0 -18
- package/dist/runtime/server/utils/remote-middleware.js +0 -0
- /package/dist/runtime/server/api/{graphql-handler.d.ts → yoga-handler.d.ts} +0 -0
package/dist/module.mjs
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { join, relative, resolve, dirname } from 'node:path';
|
|
2
|
+
import { useLogger, defineNuxtModule, createResolver, getLayerDirectories, addServerHandler, addServerImportsDir, addPlugin, addImportsDir } from '@nuxt/kit';
|
|
3
3
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { glob } from 'tinyglobby';
|
|
5
5
|
import { generate } from '@graphql-codegen/cli';
|
|
6
|
-
import { parse
|
|
7
|
-
|
|
6
|
+
import { parse, Kind } from 'graphql';
|
|
7
|
+
export { defineGraphQLContext } from '../dist/runtime/server/lib/define-graphql-context.js';
|
|
8
|
+
export { defineRemoteExecMiddleware } from '../dist/runtime/server/lib/define-remote-exec-middleware.js';
|
|
9
|
+
export { defineYogaMiddleware } from '../dist/runtime/server/lib/define-yoga-middleware.js';
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
function removeFileExtension(filePath) {
|
|
12
|
+
return filePath.replace(/\.[^/.]+$/, "");
|
|
13
|
+
}
|
|
14
|
+
function getGenericServerProxy(modulePath) {
|
|
15
|
+
return `export * from ${JSON.stringify(removeFileExtension(modulePath))};`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getGraphQLContextProxy(contextPath) {
|
|
19
|
+
if (!contextPath) {
|
|
20
|
+
return [
|
|
21
|
+
`export const getGraphQLContext = async () => ({});`,
|
|
22
|
+
`export type GraphQLContext = Awaited<ReturnType<typeof getGraphQLContext>>;`
|
|
23
|
+
].join("\n");
|
|
24
|
+
}
|
|
25
|
+
return [
|
|
26
|
+
`import context from ${JSON.stringify(removeFileExtension(contextPath))};`,
|
|
27
|
+
``,
|
|
28
|
+
`export const getGraphQLContext = context.getGraphQLContext;`,
|
|
29
|
+
`export type GraphQLContext = Awaited<ReturnType<typeof getGraphQLContext>>;`
|
|
30
|
+
].join("\n");
|
|
31
|
+
}
|
|
17
32
|
|
|
18
33
|
async function findSingleFile(dirs, pattern, isRequired = false) {
|
|
19
34
|
for (const dir of dirs) {
|
|
@@ -24,7 +39,7 @@ async function findSingleFile(dirs, pattern, isRequired = false) {
|
|
|
24
39
|
}
|
|
25
40
|
}
|
|
26
41
|
if (isRequired) {
|
|
27
|
-
throw new Error(`File not found: ${
|
|
42
|
+
throw new Error(`File not found: ${pattern} in directories:
|
|
28
43
|
${dirs.join("\n")}`);
|
|
29
44
|
}
|
|
30
45
|
}
|
|
@@ -44,299 +59,253 @@ function writeFileIfChanged(path, content) {
|
|
|
44
59
|
writeFileSync(path, content, "utf-8");
|
|
45
60
|
return true;
|
|
46
61
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (!
|
|
51
|
-
|
|
62
|
+
function toRelativePath(from, to) {
|
|
63
|
+
let relativePath = relative(resolve(from), resolve(to));
|
|
64
|
+
relativePath = relativePath.replace(/\\/g, "/");
|
|
65
|
+
if (!relativePath.startsWith("./") && !relativePath.startsWith("../")) {
|
|
66
|
+
relativePath = `./${relativePath}`;
|
|
52
67
|
}
|
|
53
|
-
return
|
|
68
|
+
return relativePath;
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return printSchema(lexicographicSortSchema(module.schema));
|
|
71
|
+
const defaultCacheConfig = {
|
|
72
|
+
cachePolicy: "cache-first",
|
|
73
|
+
cacheVersion: "1",
|
|
74
|
+
keyPrefix: "gql",
|
|
75
|
+
ttl: 60
|
|
76
|
+
};
|
|
77
|
+
function resolveCacheConfig(...overrides) {
|
|
78
|
+
return Object.assign({}, defaultCacheConfig, ...overrides);
|
|
65
79
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
zodScalars[name] = "z.coerce.number()";
|
|
95
|
-
break;
|
|
96
|
-
case "boolean":
|
|
97
|
-
zodScalars[name] = "z.coerce.boolean()";
|
|
98
|
-
break;
|
|
99
|
-
default:
|
|
100
|
-
zodScalars[name] = "z.string()";
|
|
80
|
+
|
|
81
|
+
async function runGraphQLCodegen({ schema, documents, typedDocumentsPath, zodPath }) {
|
|
82
|
+
const config = {
|
|
83
|
+
schema,
|
|
84
|
+
documents,
|
|
85
|
+
generates: {
|
|
86
|
+
// Typed document nodes
|
|
87
|
+
[typedDocumentsPath]: {
|
|
88
|
+
plugins: ["typescript", "typescript-operations", "typed-document-node"],
|
|
89
|
+
config: {
|
|
90
|
+
defaultScalarType: "never",
|
|
91
|
+
documentMode: "documentNode",
|
|
92
|
+
documentVariableSuffix: "Document",
|
|
93
|
+
enumsAsTypes: true,
|
|
94
|
+
inlineFragmentTypes: "combine",
|
|
95
|
+
preResolveTypes: false,
|
|
96
|
+
skipTypename: true,
|
|
97
|
+
strictScalars: true,
|
|
98
|
+
useTypeImports: true
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
// Zod schemas
|
|
102
|
+
[zodPath]: {
|
|
103
|
+
plugins: ["typescript-validation-schema"],
|
|
104
|
+
config: {
|
|
105
|
+
schema: "zodv4",
|
|
106
|
+
importFrom: "#graphql/operations",
|
|
107
|
+
useTypeImports: true
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
schema: "zodv4",
|
|
110
|
-
importFrom: "#graphql/operations",
|
|
111
|
-
useTypeImports: true,
|
|
112
|
-
directives: {
|
|
113
|
-
constraint: {
|
|
114
|
-
minLength: "min",
|
|
115
|
-
maxLength: "max",
|
|
116
|
-
pattern: "regex"
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
scalarSchemas: zodScalars
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
await generate({ generates, silent: true, errorsOnly: true }, true);
|
|
110
|
+
},
|
|
111
|
+
ignoreNoDocuments: true,
|
|
112
|
+
silent: true
|
|
113
|
+
};
|
|
114
|
+
const result = await generate(config, true);
|
|
115
|
+
return result.map(({ filename }) => filename);
|
|
124
116
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
|
|
118
|
+
const logger = useLogger("graphql");
|
|
119
|
+
const cyan = "\x1B[36m";
|
|
120
|
+
const reset = "\x1B[0m";
|
|
121
|
+
|
|
122
|
+
async function getRegistryContent({ layerRootDirs, rootDir, documents }) {
|
|
123
|
+
const docs = (await findMultipleFiles(layerRootDirs, documents)).map((path) => ({
|
|
124
|
+
path: toRelativePath(rootDir, path),
|
|
125
|
+
content: readFileSync(path, "utf-8")
|
|
126
|
+
}));
|
|
127
|
+
const parsed = {
|
|
128
|
+
fragments: /* @__PURE__ */ new Map(),
|
|
129
|
+
operations: {
|
|
130
|
+
query: /* @__PURE__ */ new Map(),
|
|
131
|
+
mutation: /* @__PURE__ */ new Map(),
|
|
132
|
+
subscription: /* @__PURE__ */ new Map()
|
|
133
|
+
}
|
|
132
134
|
};
|
|
133
|
-
const operationNameToFile = /* @__PURE__ */ new Map();
|
|
134
|
-
const fragmentNameToFile = /* @__PURE__ */ new Map();
|
|
135
135
|
for (const doc of docs) {
|
|
136
|
-
const ast = parse
|
|
137
|
-
const defs = [];
|
|
136
|
+
const ast = parse(doc.content);
|
|
138
137
|
for (const def of ast.definitions) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
switch (def.kind) {
|
|
139
|
+
// Process fragment definitions
|
|
140
|
+
case Kind.FRAGMENT_DEFINITION: {
|
|
141
|
+
const name = def.name.value;
|
|
142
|
+
const existing = parsed.fragments.get(name);
|
|
143
|
+
if (existing) {
|
|
144
|
+
throw new Error(`Duplicate fragment name ${name} in document ${doc.path} (previously defined in ${existing.path})`);
|
|
145
|
+
}
|
|
146
|
+
parsed.fragments.set(name, { kind: "fragment", name, path: doc.path });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
// Process operation definitions
|
|
150
|
+
case Kind.OPERATION_DEFINITION: {
|
|
151
|
+
const type = def.operation;
|
|
152
|
+
if (!["query", "mutation", "subscription"].includes(type)) continue;
|
|
153
|
+
const name = def.name?.value;
|
|
154
|
+
if (!name) throw new Error(`Unnamed ${type} operation in document ${doc.path}`);
|
|
155
|
+
const existing = parsed.operations[type].get(name);
|
|
156
|
+
if (existing) {
|
|
157
|
+
throw new Error(`Duplicate operation name ${name} in document ${doc.path} (previously defined in ${existing.path})`);
|
|
158
|
+
}
|
|
159
|
+
parsed.operations[type].set(name, { kind: "operation", type, name, path: doc.path });
|
|
160
|
+
break;
|
|
146
161
|
}
|
|
147
|
-
fragmentNameToFile.set(name2, doc.path);
|
|
148
|
-
defs.push({ kind: "fragment", name: name2 });
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (def.kind !== Kind.OPERATION_DEFINITION) continue;
|
|
152
|
-
const type = def.operation;
|
|
153
|
-
if (!["query", "mutation", "subscription"].includes(type)) continue;
|
|
154
|
-
const name = def.name?.value;
|
|
155
|
-
if (!name) {
|
|
156
|
-
throw new Error(`Unnamed ${type} operation in ${doc.path}`);
|
|
157
|
-
}
|
|
158
|
-
const prev = operationNameToFile.get(name);
|
|
159
|
-
if (prev) {
|
|
160
|
-
throw new Error(`Duplicate ${type} operation name '${name}' in:
|
|
161
|
-
- ${prev}
|
|
162
|
-
- ${doc.path}`);
|
|
163
162
|
}
|
|
164
|
-
operationNameToFile.set(name, doc.path);
|
|
165
|
-
const op = { kind: "operation", type, name };
|
|
166
|
-
defs.push(op);
|
|
167
|
-
operationsByType[type].push(op);
|
|
168
163
|
}
|
|
169
|
-
byFile.set(doc.path, defs);
|
|
170
164
|
}
|
|
171
|
-
return { byFile, operationsByType };
|
|
172
|
-
}
|
|
173
|
-
function formatDefinitions(defs) {
|
|
174
|
-
if (defs.length === 0) return "";
|
|
175
|
-
const colorOf = (def) => {
|
|
176
|
-
if (def.kind === "fragment") return green;
|
|
177
|
-
switch (def.type) {
|
|
178
|
-
case "query":
|
|
179
|
-
return blue;
|
|
180
|
-
case "mutation":
|
|
181
|
-
return magenta;
|
|
182
|
-
case "subscription":
|
|
183
|
-
return yellow;
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
return defs.map((def) => `${colorOf(def)}${def.name}${reset}`).join(`${dim} / ${reset}`);
|
|
187
|
-
}
|
|
188
|
-
function writeRegistryModule({ registryPath, operationsByType }) {
|
|
189
|
-
const queries = operationsByType.query.map((o) => o.name);
|
|
190
|
-
const mutations = operationsByType.mutation.map((o) => o.name);
|
|
191
|
-
const subscriptions = operationsByType.subscription.map((o) => o.name);
|
|
192
165
|
const content = [
|
|
193
|
-
`import
|
|
194
|
-
`import * as ops from "#graphql/operations";`,
|
|
166
|
+
`import * as ops from "#graphql/typed-documents";`,
|
|
195
167
|
``,
|
|
196
168
|
`type ResultOf<T> = T extends { __apiType?: (variables: infer _) => infer R } ? R : never;`,
|
|
197
|
-
`type VariablesOf<T> = T extends { __apiType?: (variables: infer V) => infer _ } ? V : never
|
|
169
|
+
`type VariablesOf<T> = T extends { __apiType?: (variables: infer V) => infer _ } ? V : never;`,
|
|
170
|
+
``
|
|
198
171
|
];
|
|
199
|
-
|
|
200
|
-
content.push(
|
|
201
|
-
``,
|
|
202
|
-
`// Queries`,
|
|
203
|
-
`export const queries = {`,
|
|
204
|
-
...queries.map((name) => ` ${name}: ops.${name}Document,`),
|
|
205
|
-
`} as const;`
|
|
206
|
-
);
|
|
207
|
-
} else {
|
|
208
|
-
content.push(``, `export const queries = {} as const;`);
|
|
209
|
-
}
|
|
172
|
+
const queries = Array.from(parsed.operations.query.values());
|
|
210
173
|
content.push(
|
|
211
|
-
|
|
174
|
+
`// Queries`,
|
|
175
|
+
`export const queries = {`,
|
|
176
|
+
...queries.map(({ name }) => ` ${name}: ops.${name}Document,`),
|
|
177
|
+
`};`,
|
|
212
178
|
`export type QueryName = keyof typeof queries;`,
|
|
213
179
|
`export type QueryResult<N extends QueryName> = ResultOf<(typeof queries)[N]>;`,
|
|
214
|
-
`export type QueryVariables<N extends QueryName> = VariablesOf<(typeof queries)[N]
|
|
180
|
+
`export type QueryVariables<N extends QueryName> = VariablesOf<(typeof queries)[N]>;`,
|
|
181
|
+
``
|
|
215
182
|
);
|
|
216
|
-
|
|
217
|
-
content.push(
|
|
218
|
-
``,
|
|
219
|
-
`// Mutations`,
|
|
220
|
-
`export const mutations = {`,
|
|
221
|
-
...mutations.map((name) => ` ${name}: ops.${name}Document,`),
|
|
222
|
-
`} as const;`
|
|
223
|
-
);
|
|
224
|
-
} else {
|
|
225
|
-
content.push(``, `export const mutations = {} as const;`);
|
|
226
|
-
}
|
|
183
|
+
const mutations = Array.from(parsed.operations.mutation.values());
|
|
227
184
|
content.push(
|
|
228
|
-
|
|
185
|
+
`// Mutations`,
|
|
186
|
+
`export const mutations = {`,
|
|
187
|
+
...mutations.map(({ name }) => ` ${name}: ops.${name}Document,`),
|
|
188
|
+
`};`,
|
|
229
189
|
`export type MutationName = keyof typeof mutations;`,
|
|
230
190
|
`export type MutationResult<N extends MutationName> = ResultOf<(typeof mutations)[N]>;`,
|
|
231
|
-
`export type MutationVariables<N extends MutationName> = VariablesOf<(typeof mutations)[N]
|
|
191
|
+
`export type MutationVariables<N extends MutationName> = VariablesOf<(typeof mutations)[N]>;`,
|
|
192
|
+
``
|
|
232
193
|
);
|
|
233
|
-
|
|
234
|
-
content.push(
|
|
235
|
-
``,
|
|
236
|
-
`// Subscriptions`,
|
|
237
|
-
`export const subscriptions = {`,
|
|
238
|
-
...subscriptions.map((name) => ` ${name}: ops.${name}Document,`),
|
|
239
|
-
`} as const;`
|
|
240
|
-
);
|
|
241
|
-
} else {
|
|
242
|
-
content.push(``, `export const subscriptions = {} as const;`);
|
|
243
|
-
}
|
|
194
|
+
const subscriptions = Array.from(parsed.operations.subscription.values());
|
|
244
195
|
content.push(
|
|
245
|
-
|
|
196
|
+
`// Subscriptions`,
|
|
197
|
+
`export const subscriptions = {`,
|
|
198
|
+
...subscriptions.map(({ name }) => ` ${name}: ops.${name}Document,`),
|
|
199
|
+
`};`,
|
|
246
200
|
`export type SubscriptionName = keyof typeof subscriptions;`,
|
|
247
201
|
`export type SubscriptionResult<N extends SubscriptionName> = ResultOf<(typeof subscriptions)[N]>;`,
|
|
248
|
-
`export type SubscriptionVariables<N extends SubscriptionName> = VariablesOf<(typeof subscriptions)[N]
|
|
202
|
+
`export type SubscriptionVariables<N extends SubscriptionName> = VariablesOf<(typeof subscriptions)[N]>;`,
|
|
203
|
+
``
|
|
249
204
|
);
|
|
250
|
-
return
|
|
205
|
+
return content.join("\n");
|
|
251
206
|
}
|
|
252
207
|
|
|
253
|
-
function
|
|
254
|
-
if (!
|
|
255
|
-
|
|
208
|
+
function getRemoteExecMiddlewareProxy(middlewarePath) {
|
|
209
|
+
if (!middlewarePath) {
|
|
210
|
+
return [
|
|
211
|
+
`export const remoteExecMiddlewareHandler = undefined;`
|
|
212
|
+
].join("\n");
|
|
256
213
|
}
|
|
257
|
-
|
|
258
|
-
`
|
|
214
|
+
return [
|
|
215
|
+
`import middleware from ${JSON.stringify(removeFileExtension(middlewarePath))};`,
|
|
216
|
+
``,
|
|
217
|
+
`export const remoteExecMiddlewareHandler = middleware.remoteExecMiddlewareHandler;`
|
|
259
218
|
].join("\n");
|
|
260
|
-
return writeFileIfChanged(modulePath, content);
|
|
261
219
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
headers: { "Content-Type": "application/json", ...headers },
|
|
266
|
-
body: JSON.stringify({ query: getIntrospectionQuery() })
|
|
267
|
-
});
|
|
268
|
-
const json = await response.json();
|
|
269
|
-
if (json.errors) {
|
|
270
|
-
throw new Error(`Failed to fetch remote schema from ${url}: ${JSON.stringify(json.errors)}`);
|
|
271
|
-
}
|
|
272
|
-
const schema = buildClientSchema(json.data);
|
|
273
|
-
const sdl = printSchema(schema);
|
|
274
|
-
const content = `export const sdl = /* GraphQL */ \`${sdl.replace(/`/g, "\\`")}\`;`;
|
|
275
|
-
return writeFileIfChanged(sdlPath, content);
|
|
276
|
-
}
|
|
277
|
-
function writeRemoteSchemaModule({ name, schemaDef: { url, headers }, sdlPath, modulePath, middlewarePath }) {
|
|
278
|
-
const headerSource = headers && Object.keys(headers).length > 0 ? JSON.stringify(headers, null, 2) : "{}";
|
|
279
|
-
const middlewareImport = middlewarePath ? `import middleware from ${JSON.stringify(toImportPath(modulePath, middlewarePath))};` : "";
|
|
220
|
+
|
|
221
|
+
async function getLocalSchemaProxy({ layerRootDirs, schemaDef }) {
|
|
222
|
+
const schemaPath = await findSingleFile(layerRootDirs, schemaDef.path, true);
|
|
280
223
|
const content = [
|
|
281
|
-
`
|
|
282
|
-
|
|
224
|
+
`export { schema } from ${JSON.stringify(removeFileExtension(schemaPath))};`
|
|
225
|
+
].join("\n");
|
|
226
|
+
return content;
|
|
227
|
+
}
|
|
228
|
+
async function getRemoteSchemaProxy({ rootDir, schemaName, schemaDef }) {
|
|
229
|
+
const middlewareContent = getRemoteExecMiddlewareProxy(schemaDef.middleware ? join(rootDir, schemaDef.middleware) : void 0);
|
|
230
|
+
const schema = await fetchGraphQLSchema(schemaDef.url, schemaDef.headers);
|
|
231
|
+
const sdl = await getSDLFromGraphQLSchema(schema);
|
|
232
|
+
const sdlContent = `export const sdl = /* GraphQL */ \`${sdl.replace(/`/g, "\\`")}\`;`;
|
|
233
|
+
const schemaContent = [
|
|
234
|
+
`import { buildSchema } from "graphql";`,
|
|
283
235
|
`import type { SubschemaConfig } from "@graphql-tools/delegate";`,
|
|
284
|
-
`import {
|
|
285
|
-
|
|
236
|
+
`import { remoteExecMiddlewareHandler } from "./${schemaName}-middleware";`,
|
|
237
|
+
`import { sdl } from "./${schemaName}-sdl";`,
|
|
238
|
+
`import { createRemoteExecutor } from "../remote-executor";`,
|
|
286
239
|
``,
|
|
287
|
-
`const
|
|
288
|
-
`const headers = ${headerSource} as Record<string, string>;`,
|
|
289
|
-
`const remoteName = ${JSON.stringify(name)};`,
|
|
290
|
-
`const mw = (typeof middleware === 'object' && middleware) || {};`,
|
|
240
|
+
`const headers = ${JSON.stringify(schemaDef.headers ?? {}, null, 2)} as HeadersInit;`,
|
|
291
241
|
``,
|
|
292
|
-
`const executor
|
|
293
|
-
`
|
|
294
|
-
`
|
|
295
|
-
`
|
|
296
|
-
`
|
|
297
|
-
`
|
|
298
|
-
` };`,
|
|
299
|
-
` const mwContext = { remoteName, operationName, context, fetchOptions };`,
|
|
300
|
-
` if (typeof mw.onRequest === "function") {`,
|
|
301
|
-
` const maybeOverride = await mw.onRequest(mwContext);`,
|
|
302
|
-
` if (maybeOverride && typeof maybeOverride === "object") {`,
|
|
303
|
-
` fetchOptions = maybeOverride;`,
|
|
304
|
-
` }`,
|
|
305
|
-
` }`,
|
|
306
|
-
` const response = await fetch(endpoint, fetchOptions);`,
|
|
307
|
-
` if (typeof mw.onResponse === "function") {`,
|
|
308
|
-
` await mw.onResponse({ ...mwContext, response });`,
|
|
309
|
-
` }`,
|
|
310
|
-
` return response.json();`,
|
|
311
|
-
`};`,
|
|
242
|
+
`const executor = createRemoteExecutor({`,
|
|
243
|
+
` url: ${JSON.stringify(schemaDef.url)},`,
|
|
244
|
+
` remoteName: ${JSON.stringify(schemaName)},`,
|
|
245
|
+
` headers,`,
|
|
246
|
+
` middleware: remoteExecMiddlewareHandler,`,
|
|
247
|
+
`});`,
|
|
312
248
|
``,
|
|
313
249
|
`export const schema: SubschemaConfig = {`,
|
|
314
250
|
` schema: buildSchema(sdl),`,
|
|
315
251
|
` executor,`,
|
|
316
|
-
`}
|
|
317
|
-
``
|
|
252
|
+
`};`
|
|
318
253
|
].join("\n");
|
|
319
|
-
return
|
|
254
|
+
return { middlewareContent, sdlContent, schemaContent };
|
|
320
255
|
}
|
|
321
|
-
function
|
|
322
|
-
const schemas = schemaNames.map((name) => ({
|
|
323
|
-
path: `./schemas/${name}`,
|
|
324
|
-
ref: `${name}Schema`
|
|
325
|
-
}));
|
|
256
|
+
async function getStitchedSchemaProxy({ schemaNames }) {
|
|
326
257
|
const content = [
|
|
327
|
-
`import { stitchSchemas } from "@graphql-tools/stitch"
|
|
328
|
-
`import type { GraphQLSchema } from "graphql"
|
|
329
|
-
`import type { SubschemaConfig } from "@graphql-tools/delegate"
|
|
330
|
-
...
|
|
258
|
+
`import { stitchSchemas } from "@graphql-tools/stitch"; `,
|
|
259
|
+
`import type { GraphQLSchema } from "graphql"; `,
|
|
260
|
+
`import type { SubschemaConfig } from "@graphql-tools/delegate"; `,
|
|
261
|
+
...schemaNames.map((name) => `import { schema as ${name}Schema } from ${JSON.stringify(`./schemas/${name}`)}; `),
|
|
331
262
|
``,
|
|
332
263
|
`const subschemas: Array<GraphQLSchema | SubschemaConfig> = [`,
|
|
333
|
-
...
|
|
334
|
-
`]
|
|
264
|
+
...schemaNames.map((name) => ` ${name}Schema, `),
|
|
265
|
+
`]; `,
|
|
335
266
|
``,
|
|
336
|
-
`export const schema = stitchSchemas({ subschemas })
|
|
337
|
-
|
|
267
|
+
`export const schema = stitchSchemas({ subschemas }); `
|
|
268
|
+
].join("\n");
|
|
269
|
+
return content;
|
|
270
|
+
}
|
|
271
|
+
async function loadGraphQLSchema(schemaPath) {
|
|
272
|
+
const { createJiti } = await import('jiti');
|
|
273
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
274
|
+
const module = await jiti.import(schemaPath);
|
|
275
|
+
if (!module.schema || !(module.schema instanceof Object) || typeof module.schema.getQueryType !== "function") {
|
|
276
|
+
throw new Error(`${schemaPath} must export a valid 'schema' of type GraphQLSchema.`);
|
|
277
|
+
}
|
|
278
|
+
return module.schema;
|
|
279
|
+
}
|
|
280
|
+
async function fetchGraphQLSchema(schemaUrl, headers) {
|
|
281
|
+
const { getIntrospectionQuery, buildClientSchema } = await import('graphql');
|
|
282
|
+
const response = await fetch(schemaUrl, {
|
|
283
|
+
method: "POST",
|
|
284
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
285
|
+
body: JSON.stringify({ query: getIntrospectionQuery() })
|
|
286
|
+
});
|
|
287
|
+
const json = await response.json();
|
|
288
|
+
if (json.errors) {
|
|
289
|
+
throw new Error(`Failed to fetch GraphQL schema from ${schemaUrl}: ${JSON.stringify(json.errors)} `);
|
|
290
|
+
}
|
|
291
|
+
return buildClientSchema(json.data);
|
|
292
|
+
}
|
|
293
|
+
async function getSDLFromGraphQLSchema(schema) {
|
|
294
|
+
const { printSchema, lexicographicSortSchema } = await import('graphql');
|
|
295
|
+
return printSchema(lexicographicSortSchema(schema));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function getYogaMiddlewareProxy(middlewarePath) {
|
|
299
|
+
if (!middlewarePath) {
|
|
300
|
+
return [
|
|
301
|
+
`export const yogaMiddlewareHandler = undefined;`
|
|
302
|
+
].join("\n");
|
|
303
|
+
}
|
|
304
|
+
return [
|
|
305
|
+
`import middleware from ${JSON.stringify(removeFileExtension(middlewarePath))};`,
|
|
306
|
+
``,
|
|
307
|
+
`export const yogaMiddlewareHandler = middleware.yogaMiddlewareHandler;`
|
|
338
308
|
].join("\n");
|
|
339
|
-
return writeFileIfChanged(modulePath, content);
|
|
340
309
|
}
|
|
341
310
|
|
|
342
311
|
const module$1 = defineNuxtModule({
|
|
@@ -344,136 +313,131 @@ const module$1 = defineNuxtModule({
|
|
|
344
313
|
name: "@lewebsimple/nuxt-graphql",
|
|
345
314
|
configKey: "graphql"
|
|
346
315
|
},
|
|
347
|
-
defaults: {
|
|
348
|
-
schemas: {},
|
|
349
|
-
codegen: {
|
|
350
|
-
documents: "**/*.gql",
|
|
351
|
-
saveSchema: "server/graphql/schema.graphql"
|
|
352
|
-
},
|
|
353
|
-
client: {
|
|
354
|
-
headers: {},
|
|
355
|
-
cache: {
|
|
356
|
-
enabled: false,
|
|
357
|
-
ttl: 6e4,
|
|
358
|
-
storage: "memory"
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
},
|
|
316
|
+
defaults: {},
|
|
362
317
|
async setup(options, nuxt) {
|
|
363
318
|
const { resolve } = createResolver(import.meta.url);
|
|
319
|
+
const { buildDir, rootDir } = nuxt.options;
|
|
364
320
|
const layerRootDirs = getLayerDirectories(nuxt).map(({ root }) => root);
|
|
365
|
-
const middlewarePath = resolve("./runtime/server/utils/remote-middleware");
|
|
366
|
-
const stitchedPath = join(nuxt.options.buildDir, "graphql/schema.ts");
|
|
367
|
-
const sdlPath = join(nuxt.options.rootDir, options.codegen?.saveSchema || ".nuxt/graphql/schema.graphql");
|
|
368
321
|
nuxt.options.alias ||= {};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
writeLocalSchemaModule({ localPath, modulePath: schemasPath[name] });
|
|
385
|
-
logger.info(`Local GraphQL schema "${blue}${name}${reset}" loaded from ${cyan}${relative(nuxt.options.rootDir, localPath)}${reset}`);
|
|
386
|
-
} else if (schemaDef.type === "remote") {
|
|
387
|
-
const sdlPath2 = join(nuxt.options.buildDir, `graphql/schemas/${name}-sdl.ts`);
|
|
388
|
-
if (schemaDef.middleware) {
|
|
389
|
-
middlewaresPath[name] = await findSingleFile(layerRootDirs, schemaDef.middleware, true);
|
|
390
|
-
}
|
|
391
|
-
await writeRemoteSchemaSdl({ schemaDef, sdlPath: sdlPath2 });
|
|
392
|
-
writeRemoteSchemaModule({ name, schemaDef, sdlPath: sdlPath2, modulePath: schemasPath[name], middlewarePath: middlewaresPath[name] });
|
|
393
|
-
logger.info(`Remote GraphQL schema "${magenta}${name}${reset}" loaded from ${cyan}${schemaDef.url}${reset}`);
|
|
394
|
-
} else {
|
|
395
|
-
throw new Error(`Unknown schema type for schema '${name}'`);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
writeStitchedSchemaModule({ schemaNames: Object.keys(options.schemas), modulePath: stitchedPath });
|
|
399
|
-
nuxt.hook("nitro:config", (config) => {
|
|
400
|
-
config.alias ||= {};
|
|
401
|
-
config.alias["#graphql/context"] = contextPath;
|
|
402
|
-
config.alias["#graphql/middleware"] = middlewarePath;
|
|
403
|
-
for (const name of Object.keys(options.schemas)) {
|
|
404
|
-
config.alias[`#graphql/schemas/${name}`] = schemasPath[name];
|
|
322
|
+
nuxt.options.runtimeConfig.public.graphql = {
|
|
323
|
+
cacheConfig: resolveCacheConfig(options.cache)
|
|
324
|
+
};
|
|
325
|
+
const serverProxies = {};
|
|
326
|
+
const contextPath = options.context ? await findSingleFile(layerRootDirs, options.context, true) : void 0;
|
|
327
|
+
if (contextPath) {
|
|
328
|
+
logger.info(`GraphQL context registered: ${cyan}${toRelativePath(rootDir, contextPath)}${reset}`);
|
|
329
|
+
}
|
|
330
|
+
serverProxies["context"] = getGraphQLContextProxy(contextPath);
|
|
331
|
+
serverProxies["remote-executor"] = getGenericServerProxy(resolve("./runtime/server/lib/remote-executor.ts"));
|
|
332
|
+
for (const [schemaName, schemaDef] of Object.entries(options.schemas)) {
|
|
333
|
+
switch (schemaDef.type) {
|
|
334
|
+
case "local": {
|
|
335
|
+
serverProxies[`schemas/${schemaName}`] = await getLocalSchemaProxy({ layerRootDirs, schemaDef });
|
|
336
|
+
break;
|
|
405
337
|
}
|
|
406
|
-
|
|
407
|
-
|
|
338
|
+
case "remote": {
|
|
339
|
+
const { middlewareContent, sdlContent, schemaContent } = await getRemoteSchemaProxy({ rootDir, schemaName, schemaDef });
|
|
340
|
+
serverProxies[`schemas/${schemaName}-middleware`] = middlewareContent;
|
|
341
|
+
serverProxies[`schemas/${schemaName}-sdl`] = sdlContent;
|
|
342
|
+
serverProxies[`schemas/${schemaName}`] = schemaContent;
|
|
343
|
+
break;
|
|
408
344
|
}
|
|
409
|
-
|
|
410
|
-
|
|
345
|
+
default:
|
|
346
|
+
throw new Error(`Unsupported GraphQL schema type: ${schemaDef.type}`);
|
|
347
|
+
}
|
|
411
348
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const relativePath = relative(nuxt.options.rootDir, path);
|
|
427
|
-
logger.info(`${cyan}${relativePath}${reset} [${formatDefinitions(defs)}]`);
|
|
428
|
-
});
|
|
429
|
-
writeRegistryModule({ registryPath, operationsByType });
|
|
430
|
-
const config = {
|
|
431
|
-
schema: relative(nuxt.options.rootDir, sdlPath),
|
|
432
|
-
documents: documentsPattern
|
|
433
|
-
};
|
|
434
|
-
writeFileIfChanged(configPath, JSON.stringify(config, null, 2));
|
|
435
|
-
} catch (error) {
|
|
436
|
-
logger.warn(`GraphQL codegen failed: ${error.message}`);
|
|
437
|
-
}
|
|
349
|
+
serverProxies["schema"] = await getStitchedSchemaProxy({ schemaNames: Object.keys(options.schemas) });
|
|
350
|
+
if (options.middleware) {
|
|
351
|
+
const yogaMiddlewarePath = await findSingleFile(layerRootDirs, options.middleware, true);
|
|
352
|
+
logger.info(`GraphQL Yoga middleware registered: ${cyan}${toRelativePath(nuxt.options.rootDir, yogaMiddlewarePath)}${reset}`);
|
|
353
|
+
serverProxies["yoga-middleware"] = getYogaMiddlewareProxy(yogaMiddlewarePath);
|
|
354
|
+
} else {
|
|
355
|
+
serverProxies["yoga-middleware"] = getYogaMiddlewareProxy();
|
|
356
|
+
}
|
|
357
|
+
const sdlPath = options.saveSdl ? join(rootDir, options.saveSdl) : join(buildDir, "graphql/schema.graphql");
|
|
358
|
+
async function generateGraphQLSDL() {
|
|
359
|
+
const schema = await loadGraphQLSchema(join(buildDir, "graphql/schema.ts"));
|
|
360
|
+
const sdlContent = await getSDLFromGraphQLSchema(schema);
|
|
361
|
+
if (writeFileIfChanged(sdlPath, sdlContent)) {
|
|
362
|
+
logger.info(`GraphQL SDL generated: ${cyan}${toRelativePath(rootDir, sdlPath)}${reset}`);
|
|
438
363
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
nuxt.hook("builder:watch", async (event, path) => {
|
|
447
|
-
if (path.endsWith(".gql")) {
|
|
448
|
-
await generate();
|
|
449
|
-
}
|
|
450
|
-
});
|
|
364
|
+
}
|
|
365
|
+
const configPath = join(rootDir, options.saveConfig || "graphql.config.json");
|
|
366
|
+
const documents = options.documents || "**/*.gql";
|
|
367
|
+
async function generateGraphQLConfig() {
|
|
368
|
+
const configContent = JSON.stringify({ schema: toRelativePath(rootDir, sdlPath), documents }, null, 2);
|
|
369
|
+
if (writeFileIfChanged(configPath, configContent)) {
|
|
370
|
+
logger.info(`GraphQL Config generated: ${cyan}${toRelativePath(rootDir, configPath)}${reset}`);
|
|
451
371
|
}
|
|
452
372
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
373
|
+
const typedDocumentsPath = join(buildDir, "graphql/typed-documents.ts");
|
|
374
|
+
nuxt.options.alias["#graphql/typed-documents"] = typedDocumentsPath;
|
|
375
|
+
const zodPath = join(buildDir, "graphql/zod.ts");
|
|
376
|
+
nuxt.options.alias["#graphql/zod"] = zodPath;
|
|
377
|
+
async function generateGraphQLCodegen() {
|
|
378
|
+
try {
|
|
379
|
+
const files = await runGraphQLCodegen({ schema: sdlPath, documents, typedDocumentsPath, zodPath });
|
|
380
|
+
for (const file of files) {
|
|
381
|
+
logger.info(`GraphQL Codegen generated: ${cyan}${toRelativePath(rootDir, file)}${reset}`);
|
|
382
|
+
}
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (!nuxt.options.dev) {
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
const message = error instanceof AggregateError ? error.errors[0].message : String(error);
|
|
388
|
+
logger.warn(message);
|
|
389
|
+
}
|
|
458
390
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
391
|
+
const registryPath = join(buildDir, "graphql/registry.ts");
|
|
392
|
+
nuxt.options.alias["#graphql/registry"] = registryPath;
|
|
393
|
+
async function generateGraphQLRegistry() {
|
|
394
|
+
const registryContent = await getRegistryContent({ layerRootDirs, rootDir, documents });
|
|
395
|
+
writeFileIfChanged(registryPath, registryContent);
|
|
396
|
+
}
|
|
397
|
+
let artifactsPromise;
|
|
398
|
+
async function generateGraphQLArtifacts() {
|
|
399
|
+
if (!artifactsPromise) {
|
|
400
|
+
artifactsPromise = (async () => {
|
|
401
|
+
await generateGraphQLSDL();
|
|
402
|
+
await generateGraphQLConfig();
|
|
403
|
+
await generateGraphQLCodegen();
|
|
404
|
+
await generateGraphQLRegistry();
|
|
405
|
+
})();
|
|
406
|
+
}
|
|
407
|
+
return artifactsPromise;
|
|
408
|
+
}
|
|
409
|
+
addServerHandler({ route: "/api/graphql", handler: resolve("./runtime/server/api/yoga-handler") });
|
|
410
|
+
nuxt.hook("listen", (_, { url }) => {
|
|
411
|
+
logger.success(`GraphQL Yoga ready: ${cyan}${url.replace(/\/$/, "")}/api/graphql${reset}`);
|
|
412
|
+
});
|
|
413
|
+
addServerImportsDir(resolve("./runtime/server/utils"));
|
|
414
|
+
addPlugin(resolve("./runtime/app/plugins/graphql-request"));
|
|
415
|
+
addPlugin(resolve("./runtime/app/plugins/graphql-sse.client"));
|
|
416
|
+
addImportsDir(resolve("./runtime/app/composables"));
|
|
417
|
+
nuxt.hook("nitro:config", async (nitroConfig) => {
|
|
418
|
+
nitroConfig.alias ||= {};
|
|
419
|
+
for (const [proxyName, proxyContent] of Object.entries(serverProxies)) {
|
|
420
|
+
const proxyPath = join(buildDir, "graphql", `${proxyName}.ts`);
|
|
421
|
+
nitroConfig.alias[`#graphql/${proxyName}`] = proxyPath;
|
|
422
|
+
writeFileIfChanged(proxyPath, proxyContent);
|
|
423
|
+
}
|
|
424
|
+
await generateGraphQLArtifacts();
|
|
425
|
+
});
|
|
426
|
+
nuxt.hook("prepare:types", async ({ references }) => {
|
|
427
|
+
await generateGraphQLArtifacts();
|
|
428
|
+
references.push({ path: registryPath });
|
|
429
|
+
references.push({ path: typedDocumentsPath });
|
|
430
|
+
references.push({ path: zodPath });
|
|
431
|
+
});
|
|
432
|
+
if (nuxt.options.dev) {
|
|
433
|
+
nuxt.hook("builder:watch", async (_event, path) => {
|
|
434
|
+
if (path.endsWith(".gql")) {
|
|
435
|
+
logger.info(`GraphQL document change detected: ${cyan}${toRelativePath(nuxt.options.rootDir, path)}${reset}`);
|
|
436
|
+
await generateGraphQLCodegen();
|
|
437
|
+
await generateGraphQLRegistry();
|
|
467
438
|
}
|
|
468
|
-
};
|
|
469
|
-
addPlugin(resolve("./runtime/app/plugins/graphql"));
|
|
470
|
-
addImportsDir(resolve("./runtime/app/composables"));
|
|
471
|
-
addServerImportsDir(resolve("./runtime/server/utils"));
|
|
439
|
+
});
|
|
472
440
|
}
|
|
473
|
-
await setupContextSchemas();
|
|
474
|
-
await setupCodegen();
|
|
475
|
-
setupYogaHandler();
|
|
476
|
-
setupClient();
|
|
477
441
|
}
|
|
478
442
|
});
|
|
479
443
|
|