@nxgt/shared-graphql 2.1.0 → 3.0.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/README.md +112 -52
- package/dist/directives/authenticated.d.ts +42 -0
- package/dist/directives/authenticated.d.ts.map +1 -0
- package/dist/directives/federation.d.ts +6 -0
- package/dist/directives/federation.d.ts.map +1 -1
- package/dist/directives/index.d.ts +1 -2
- package/dist/directives/index.d.ts.map +1 -1
- package/dist/directives/permission.d.ts +20 -6
- package/dist/directives/permission.d.ts.map +1 -1
- package/dist/directives/removed-check.d.ts +15 -0
- package/dist/directives/removed-check.d.ts.map +1 -0
- package/dist/directives/scope.d.ts +3 -3
- package/dist/directives/scope.d.ts.map +1 -1
- package/dist/directives/sdl.d.ts +14 -4
- package/dist/directives/sdl.d.ts.map +1 -1
- package/dist/directives/validate.d.ts +8 -14
- package/dist/directives/validate.d.ts.map +1 -1
- package/dist/index.js +1037 -926
- package/dist/index.js.map +35 -31
- package/dist/integrations/hono.d.ts +2 -2
- package/dist/integrations/hono.d.ts.map +1 -1
- package/dist/plugins/apply-authenticated.d.ts +21 -0
- package/dist/plugins/apply-authenticated.d.ts.map +1 -0
- package/dist/plugins/apply-keto-checks.d.ts +10 -7
- package/dist/plugins/apply-keto-checks.d.ts.map +1 -1
- package/dist/plugins/auth.d.ts +17 -1
- package/dist/plugins/auth.d.ts.map +1 -1
- package/dist/plugins/authenticated.d.ts +14 -0
- package/dist/plugins/authenticated.d.ts.map +1 -0
- package/dist/plugins/extract-jwt.d.ts +19 -6
- package/dist/plugins/extract-jwt.d.ts.map +1 -1
- package/dist/plugins/field-guard.d.ts +30 -0
- package/dist/plugins/field-guard.d.ts.map +1 -0
- package/dist/plugins/gateway-trust.d.ts +15 -0
- package/dist/plugins/gateway-trust.d.ts.map +1 -0
- package/dist/plugins/index.d.ts +3 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/keto-checks.d.ts +3 -2
- package/dist/plugins/keto-checks.d.ts.map +1 -1
- package/dist/plugins/keto-helpers.d.ts +1 -1
- package/dist/plugins/keto-helpers.d.ts.map +1 -1
- package/dist/plugins/ory-auth.d.ts +3 -3
- package/dist/shared.d.ts +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/utils/errors/denial.d.ts +20 -0
- package/dist/utils/errors/denial.d.ts.map +1 -0
- package/dist/utils/errors/format-error.d.ts +2 -1
- package/dist/utils/errors/format-error.d.ts.map +1 -1
- package/dist/utils/errors/index.d.ts +1 -0
- package/dist/utils/errors/index.d.ts.map +1 -1
- package/dist/utils/errors/mask-error.d.ts +2 -1
- package/dist/utils/errors/mask-error.d.ts.map +1 -1
- package/dist/utils/ws-context.d.ts +1 -1
- package/docs/README.md +8 -6
- package/docs/guide/authentication.md +187 -0
- package/docs/guide/errors.md +37 -5
- package/docs/guide/migrating-to-3.md +276 -0
- package/docs/guide/permissions.md +39 -50
- package/docs/roadmap.md +30 -29
- package/docs/troubleshooting.md +151 -48
- package/package.json +7 -11
- package/dist/directives/check.d.ts +0 -32
- package/dist/directives/check.d.ts.map +0 -1
- package/dist/directives/deprecation.d.ts +0 -7
- package/dist/directives/deprecation.d.ts.map +0 -1
- package/dist/directives/requirements.d.ts +0 -14
- package/dist/directives/requirements.d.ts.map +0 -1
- package/graphql/directives/check.graphqls +0 -90
package/dist/index.js
CHANGED
|
@@ -3,15 +3,56 @@ import DataLoader3 from "dataloader";
|
|
|
3
3
|
|
|
4
4
|
export * from "graphql-subscriptions";
|
|
5
5
|
|
|
6
|
-
// src/directives/
|
|
6
|
+
// src/directives/authenticated.ts
|
|
7
7
|
import { getDirective } from "@graphql-tools/utils";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
var AUTHENTICATED_DIRECTIVE_NAME = "authenticated";
|
|
9
|
+
var CALLER_TYPES = ["session", "token"];
|
|
10
|
+
function readAuthenticated(schema, node, where, known) {
|
|
11
|
+
const [found] = getDirective(schema, node, AUTHENTICATED_DIRECTIVE_NAME) ?? [];
|
|
12
|
+
if (!found)
|
|
13
|
+
return null;
|
|
14
|
+
const types = found.type;
|
|
15
|
+
if (types === undefined || types === null)
|
|
16
|
+
return {};
|
|
17
|
+
if (!Array.isArray(types) || types.length === 0) {
|
|
18
|
+
throw new TypeError(`@authenticated on ${where}: \`type: []\` admits no caller — name a type, or drop \`type\``);
|
|
19
|
+
}
|
|
20
|
+
for (const type of types) {
|
|
21
|
+
if (!known.includes(type)) {
|
|
22
|
+
throw new TypeError(`@authenticated on ${where}: unknown type ${JSON.stringify(type)} — known: ${known.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { types };
|
|
13
26
|
}
|
|
14
|
-
|
|
27
|
+
function combineRequirements(found, where) {
|
|
28
|
+
const applying = found.filter((one) => !!one);
|
|
29
|
+
if (applying.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
let types;
|
|
32
|
+
for (const { types: these } of applying) {
|
|
33
|
+
if (!these)
|
|
34
|
+
continue;
|
|
35
|
+
types = types ? types.filter((type) => these.includes(type)) : these;
|
|
36
|
+
}
|
|
37
|
+
if (types?.length === 0) {
|
|
38
|
+
throw new TypeError(`@authenticated on ${where}: the field's, its type's and its interfaces' \`type\`s have none in common — no caller could pass`);
|
|
39
|
+
}
|
|
40
|
+
return types ? { types } : {};
|
|
41
|
+
}
|
|
42
|
+
// src/directives/federation.ts
|
|
43
|
+
var FEDERATION_DIRECTIVES = `
|
|
44
|
+
directive @external on FIELD_DEFINITION | OBJECT
|
|
45
|
+
directive @requires(fields: FieldSet!) on FIELD_DEFINITION
|
|
46
|
+
directive @provides(fields: FieldSet!) on FIELD_DEFINITION
|
|
47
|
+
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
|
|
48
|
+
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
|
|
49
|
+
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
|
|
50
|
+
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
|
|
51
|
+
directive @override(from: String!) on FIELD_DEFINITION
|
|
52
|
+
directive @composeDirective(name: String!) repeatable on SCHEMA
|
|
53
|
+
directive @interfaceObject on OBJECT
|
|
54
|
+
directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
|
|
55
|
+
`;
|
|
15
56
|
// src/directives/paths.ts
|
|
16
57
|
var PATH = /^(args|parent|source)(\.[A-Za-z0-9_]+)+$/;
|
|
17
58
|
var DEFAULT_ID_PATH = "args.id";
|
|
@@ -39,6 +80,8 @@ function objectIds(value) {
|
|
|
39
80
|
const values = Array.isArray(value) ? value : [value];
|
|
40
81
|
return values.filter((item) => typeof item === "string" && item.length > 0);
|
|
41
82
|
}
|
|
83
|
+
// src/directives/permission.ts
|
|
84
|
+
import { getDirective as getDirective2 } from "@graphql-tools/utils";
|
|
42
85
|
|
|
43
86
|
// src/directives/scope.ts
|
|
44
87
|
function scopeOf(node, where, options) {
|
|
@@ -69,64 +112,15 @@ function assertRequirementShape(permissions, scope) {
|
|
|
69
112
|
for (const group of permissions) {
|
|
70
113
|
for (const term of group) {
|
|
71
114
|
assertReadablePath(term, where);
|
|
72
|
-
|
|
115
|
+
assertDeclaredArgument(term, argumentNames, where);
|
|
73
116
|
if (namespaces && !namespaces.includes(term.namespace)) {
|
|
74
117
|
throw new TypeError(`${where}: unknown namespace ${JSON.stringify(term.namespace)} — known: ${namespaces.join(", ")}`);
|
|
75
118
|
}
|
|
76
119
|
}
|
|
77
120
|
}
|
|
78
121
|
}
|
|
79
|
-
function declaredOrReported(term, argumentNames, scope) {
|
|
80
|
-
if (!scope.lenient) {
|
|
81
|
-
assertDeclaredArgument(term, argumentNames, scope.where);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
assertDeclaredArgument(term, argumentNames, scope.where);
|
|
86
|
-
} catch (error) {
|
|
87
|
-
scope.lenient(error.message);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
122
|
|
|
91
|
-
// src/directives/check.ts
|
|
92
|
-
var CHECK_DIRECTIVE_NAME = "check";
|
|
93
|
-
function readChecks(schema, node, where, options = {}) {
|
|
94
|
-
const found = getDirective(schema, node, CHECK_DIRECTIVE_NAME) ?? [];
|
|
95
|
-
const scope = {
|
|
96
|
-
...scopeOf(node, `@check on ${where}`, options),
|
|
97
|
-
lenient: warnCheckMistake
|
|
98
|
-
};
|
|
99
|
-
return found.map((raw) => {
|
|
100
|
-
const permissions = withDefaultIds(raw.permissions);
|
|
101
|
-
assertRequirementShape(permissions, scope);
|
|
102
|
-
return {
|
|
103
|
-
permissions,
|
|
104
|
-
onDeny: raw.onDeny ?? "NOT_FOUND",
|
|
105
|
-
message: raw.message ?? undefined
|
|
106
|
-
};
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
function withDefaultIds(raw) {
|
|
110
|
-
if (!Array.isArray(raw))
|
|
111
|
-
return raw;
|
|
112
|
-
return raw.map((group) => Array.isArray(group) ? group.map((term) => term && typeof term === "object" ? { ...term, id: term.id ?? DEFAULT_ID_PATH } : term) : group);
|
|
113
|
-
}
|
|
114
|
-
// src/directives/federation.ts
|
|
115
|
-
var FEDERATION_DIRECTIVES = `
|
|
116
|
-
directive @external on FIELD_DEFINITION | OBJECT
|
|
117
|
-
directive @requires(fields: FieldSet!) on FIELD_DEFINITION
|
|
118
|
-
directive @provides(fields: FieldSet!) on FIELD_DEFINITION
|
|
119
|
-
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
|
|
120
|
-
directive @shareable repeatable on OBJECT | FIELD_DEFINITION
|
|
121
|
-
directive @inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
|
|
122
|
-
directive @tag(name: String!) repeatable on FIELD_DEFINITION | INTERFACE | OBJECT | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
|
|
123
|
-
directive @override(from: String!) on FIELD_DEFINITION
|
|
124
|
-
directive @composeDirective(name: String!) repeatable on SCHEMA
|
|
125
|
-
directive @interfaceObject on OBJECT
|
|
126
|
-
directive @authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
|
|
127
|
-
`;
|
|
128
123
|
// src/directives/permission.ts
|
|
129
|
-
import { getDirective as getDirective2 } from "@graphql-tools/utils";
|
|
130
124
|
var PERMISSION_DIRECTIVE_NAME = "permission";
|
|
131
125
|
function readPermissions(schema, node, where, options = {}) {
|
|
132
126
|
if (!isOurs(schema))
|
|
@@ -156,109 +150,7 @@ function isOurs(schema) {
|
|
|
156
150
|
const names = declared?.args.map((arg) => arg.name) ?? [];
|
|
157
151
|
return names.includes("name") && names.includes("type");
|
|
158
152
|
}
|
|
159
|
-
// src/directives/requirements.ts
|
|
160
|
-
function readRequirements(schema, node, where, options = {}) {
|
|
161
|
-
const queues = {
|
|
162
|
-
[CHECK_DIRECTIVE_NAME]: readChecks(schema, node, where, options),
|
|
163
|
-
[PERMISSION_DIRECTIVE_NAME]: readPermissions(schema, node, where, options)
|
|
164
|
-
};
|
|
165
|
-
const ordered = [];
|
|
166
|
-
for (const directive of directivesOf(node)) {
|
|
167
|
-
const next = queues[directive.name.value]?.shift();
|
|
168
|
-
if (next)
|
|
169
|
-
ordered.push(next);
|
|
170
|
-
}
|
|
171
|
-
return [
|
|
172
|
-
...ordered,
|
|
173
|
-
...queues[CHECK_DIRECTIVE_NAME] ?? [],
|
|
174
|
-
...queues[PERMISSION_DIRECTIVE_NAME] ?? []
|
|
175
|
-
];
|
|
176
|
-
}
|
|
177
|
-
function directivesOf(node) {
|
|
178
|
-
const field = node;
|
|
179
|
-
return field.astNode?.directives ?? field.directives ?? [];
|
|
180
|
-
}
|
|
181
153
|
// src/directives/sdl.ts
|
|
182
|
-
var CHECK_DIRECTIVE_SDL = `"""
|
|
183
|
-
One question, with the object left as a path instead of a value.
|
|
184
|
-
"""
|
|
185
|
-
input CheckPermission {
|
|
186
|
-
"""
|
|
187
|
-
The Keto namespace, e.g. "Note" — from the stack's OPL document.
|
|
188
|
-
"""
|
|
189
|
-
namespace: String!
|
|
190
|
-
|
|
191
|
-
"""
|
|
192
|
-
The permit asked of it, e.g. "view". A plain relation works too; Keto
|
|
193
|
-
answers \`false\`, not an error, for a name it does not know.
|
|
194
|
-
"""
|
|
195
|
-
permit: String!
|
|
196
|
-
|
|
197
|
-
"""
|
|
198
|
-
Where the object id is read: \`args.<path>\`, or \`parent.<path>\` /
|
|
199
|
-
\`source.<path>\` (the same root under two names), dotted paths allowed. A value that turns out to be a LIST requires the permit on every
|
|
200
|
-
element — that is what a mutation taking \`ids: [ID!]!\` means.
|
|
201
|
-
"""
|
|
202
|
-
id: String = "args.id"
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
"""
|
|
206
|
-
What a denial looks like from outside.
|
|
207
|
-
"""
|
|
208
|
-
enum CheckDenial {
|
|
209
|
-
"""
|
|
210
|
-
The same answer as for an id that never existed, so ids cannot be probed.
|
|
211
|
-
The default, and the one to keep unless the caller already knows the object
|
|
212
|
-
is there.
|
|
213
|
-
"""
|
|
214
|
-
NOT_FOUND
|
|
215
|
-
|
|
216
|
-
"""
|
|
217
|
-
For a second check on an object the caller can already see: a viewer asked
|
|
218
|
-
to edit already knows it exists, and "you may not change it" is honest.
|
|
219
|
-
"""
|
|
220
|
-
FORBIDDEN
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
"""
|
|
224
|
-
Deprecated: write \`@permission(name:, type:)\` in a new schema — one Keto
|
|
225
|
-
question per directive, repeated for AND, with OR kept in the Keto model. This
|
|
226
|
-
form keeps working and is evaluated in declaration order with @permission.
|
|
227
|
-
|
|
228
|
-
The permission a field requires, in disjunctive normal form: the OUTER list is
|
|
229
|
-
OR, the INNER list is AND. \`[[A, B], [C]]\` reads "(A and B) or C" — the same
|
|
230
|
-
shape \`@policy(policies: [["ADMIN"]])\` uses.
|
|
231
|
-
|
|
232
|
-
Repeatable, and evaluated in declaration order, each with its own \`onDeny\`.
|
|
233
|
-
That is how the 404-then-403 ladder is written:
|
|
234
|
-
|
|
235
|
-
updateNote(id: ID!, input: UpdateNoteInput!): Note!
|
|
236
|
-
@check(permissions: [[{ namespace: "Note", permit: "view" }]])
|
|
237
|
-
@check(permissions: [[{ namespace: "Note", permit: "edit" }]], onDeny: FORBIDDEN)
|
|
238
|
-
|
|
239
|
-
NOT for a field that answers a LIST the caller is entitled to. "Which notes may
|
|
240
|
-
I see" is not a check, it is a Keto query folded into the database filter
|
|
241
|
-
before the read. A directive there would have to fetch everything and filter
|
|
242
|
-
after, which makes \`totalCount\` and the cursors lie.
|
|
243
|
-
"""
|
|
244
|
-
directive @check(
|
|
245
|
-
permissions: [[CheckPermission!]!]!
|
|
246
|
-
onDeny: CheckDenial! = NOT_FOUND
|
|
247
|
-
|
|
248
|
-
"""
|
|
249
|
-
The i18n key the denial carries, e.g. "notes.errors.not-found". Defaults to
|
|
250
|
-
\`errors.not-found\` / \`errors.insufficient-permissions\`, the shared keys.
|
|
251
|
-
|
|
252
|
-
Set it whenever the API's own service layer answers the same refusal with a
|
|
253
|
-
domain message. Two layers guard these fields — the directive, and the
|
|
254
|
-
\`require<M>Access\` the service calls — and if they word the same 404
|
|
255
|
-
differently, the wording tells a caller WHICH one refused: a generic message
|
|
256
|
-
means "you may not", a domain one means "it is gone". That is precisely the
|
|
257
|
-
distinction NOT_FOUND exists to hide.
|
|
258
|
-
"""
|
|
259
|
-
message: String
|
|
260
|
-
) repeatable on FIELD_DEFINITION
|
|
261
|
-
`;
|
|
262
154
|
var PERMISSION_DIRECTIVE_SDL = `"""
|
|
263
155
|
What a refused @permission looks like from outside.
|
|
264
156
|
"""
|
|
@@ -321,222 +213,425 @@ directive @permission(
|
|
|
321
213
|
message: String
|
|
322
214
|
) repeatable on FIELD_DEFINITION
|
|
323
215
|
`;
|
|
324
|
-
var KETO_DIRECTIVES_SDL =
|
|
325
|
-
|
|
216
|
+
var KETO_DIRECTIVES_SDL = PERMISSION_DIRECTIVE_SDL;
|
|
217
|
+
var AUTHENTICATED_DIRECTIVE_SDL = `"""
|
|
218
|
+
A signed-in caller — of one of the types \`type\` names, when it names some.
|
|
219
|
+
"""
|
|
220
|
+
directive @authenticated(
|
|
221
|
+
"""
|
|
222
|
+
The kinds of caller admitted: "session" or "token" by default. Any caller
|
|
223
|
+
when omitted.
|
|
224
|
+
"""
|
|
225
|
+
type: [String!]
|
|
226
|
+
) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
|
|
227
|
+
`;
|
|
326
228
|
// src/integrations/hono.ts
|
|
327
229
|
import { Hono } from "hono";
|
|
328
230
|
import { contextStorage } from "hono/context-storage";
|
|
329
231
|
import { createMiddleware } from "hono/factory";
|
|
330
232
|
import { languageDetector } from "hono/language";
|
|
331
233
|
|
|
332
|
-
// src/utils/
|
|
234
|
+
// src/utils/sandbox.ts
|
|
235
|
+
import { html } from "hono/html";
|
|
236
|
+
var renderSandbox = ({
|
|
237
|
+
port = 8080,
|
|
238
|
+
hostname = "localhost",
|
|
239
|
+
graphqlEndpoint = "graphql",
|
|
240
|
+
protocol = "http",
|
|
241
|
+
title = "Sandbox Explorer"
|
|
242
|
+
}) => html`
|
|
243
|
+
<title>${title}</title>
|
|
244
|
+
<div id="sandbox" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
|
|
245
|
+
<script src="https://embeddable-sandbox.cdn.apollographql.com/_latest/embeddable-sandbox.umd.production.min.js"></script>
|
|
246
|
+
<script>
|
|
247
|
+
new window.EmbeddedSandbox({
|
|
248
|
+
target: "#sandbox",
|
|
249
|
+
// Pass through your server href if you are embedding on an endpoint.
|
|
250
|
+
// Otherwise, you can pass whatever endpoint you want Sandbox to start up with here.
|
|
251
|
+
initialEndpoint: "${protocol}://${hostname}:${port}/${graphqlEndpoint}",
|
|
252
|
+
handleRequest: (endpointUrl, options) => {
|
|
253
|
+
return fetch(endpointUrl, {
|
|
254
|
+
...options,
|
|
255
|
+
headers: {
|
|
256
|
+
...options.headers
|
|
257
|
+
},
|
|
258
|
+
})
|
|
259
|
+
},
|
|
260
|
+
hideCookieToggle: true,
|
|
261
|
+
});
|
|
262
|
+
// advanced options: https://www.apollographql.com/docs/studio/explorer/sandbox#embedding-sandbox
|
|
263
|
+
</script>
|
|
264
|
+
|
|
265
|
+
`;
|
|
266
|
+
|
|
267
|
+
// src/integrations/hono.ts
|
|
268
|
+
function sandboxExplorer(options) {
|
|
269
|
+
return createMiddleware(async (ctx) => {
|
|
270
|
+
return ctx.html(renderSandbox(options));
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function honoYoga(yoga) {
|
|
274
|
+
return createMiddleware(async (ctx) => {
|
|
275
|
+
const response = await yoga.fetch(ctx.req.raw);
|
|
276
|
+
return response;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function createYogaHono(yoga, options) {
|
|
280
|
+
const app = new Hono;
|
|
281
|
+
app.use(contextStorage());
|
|
282
|
+
app.use(languageDetector({
|
|
283
|
+
order: ["header"],
|
|
284
|
+
supportedLanguages: ["en", "fr"],
|
|
285
|
+
fallbackLanguage: "en",
|
|
286
|
+
caches: []
|
|
287
|
+
}));
|
|
288
|
+
app.get("/health", (ctx) => {
|
|
289
|
+
return ctx.json({ status: "ok" }, 200);
|
|
290
|
+
});
|
|
291
|
+
app.get(options?.sandbox?.endpoint || "sandbox", sandboxExplorer(options?.sandbox || {}));
|
|
292
|
+
app.use(yoga.graphqlEndpoint, honoYoga(yoga));
|
|
293
|
+
return app;
|
|
294
|
+
}
|
|
295
|
+
// src/plugins/apply-authenticated.ts
|
|
296
|
+
import { MapperKind, mapSchema } from "@graphql-tools/utils";
|
|
297
|
+
import { ErrorCode as ErrorCode2 } from "@nxgt/shared-exceptions";
|
|
333
298
|
import {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
299
|
+
getNamedType,
|
|
300
|
+
isEnumType,
|
|
301
|
+
isObjectType,
|
|
302
|
+
isScalarType
|
|
303
|
+
} from "graphql";
|
|
304
|
+
|
|
305
|
+
// src/utils/errors/denial.ts
|
|
306
|
+
import { translate } from "@nxgt/i18n";
|
|
307
|
+
import { ErrorCode } from "@nxgt/shared-exceptions";
|
|
308
|
+
import { GraphQLError } from "graphql";
|
|
309
|
+
var STATUS = {
|
|
310
|
+
[ErrorCode.Unauthenticated]: 401,
|
|
311
|
+
[ErrorCode.Forbidden]: 403,
|
|
312
|
+
[ErrorCode.NotFound]: 404
|
|
313
|
+
};
|
|
314
|
+
var DEFAULT_MESSAGE = {
|
|
315
|
+
[ErrorCode.Unauthenticated]: "errors.unauthenticated",
|
|
316
|
+
[ErrorCode.Forbidden]: "errors.insufficient-permissions",
|
|
317
|
+
[ErrorCode.NotFound]: "errors.not-found"
|
|
318
|
+
};
|
|
319
|
+
var MESSAGE_KEY = Symbol.for("@nxgt/shared-graphql/denial-message");
|
|
320
|
+
function denial(code, message) {
|
|
321
|
+
const key = message ?? DEFAULT_MESSAGE[code];
|
|
322
|
+
const error = new GraphQLError(translate(key), {
|
|
323
|
+
extensions: { code, http: { status: STATUS[code] } }
|
|
324
|
+
});
|
|
325
|
+
Object.defineProperty(error, MESSAGE_KEY, { value: key });
|
|
326
|
+
return error;
|
|
327
|
+
}
|
|
328
|
+
function denialMessageKey(error) {
|
|
329
|
+
if (!error || typeof error !== "object")
|
|
330
|
+
return;
|
|
331
|
+
const key = error[MESSAGE_KEY];
|
|
332
|
+
return typeof key === "string" ? key : undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/plugins/field-guard.ts
|
|
336
|
+
import {
|
|
337
|
+
defaultFieldResolver
|
|
338
|
+
} from "graphql";
|
|
339
|
+
var GUARDS = "@nxgt/shared-graphql:guards";
|
|
340
|
+
function intactGuards(config) {
|
|
341
|
+
const record = config.extensions?.[GUARDS];
|
|
342
|
+
if (!record || record.wrapped.resolve !== config.resolve)
|
|
343
|
+
return;
|
|
344
|
+
if (record.wrapped.subscribe !== config.subscribe)
|
|
345
|
+
return;
|
|
346
|
+
return record;
|
|
347
|
+
}
|
|
348
|
+
function isGuarded(config, mark) {
|
|
349
|
+
return !!intactGuards(config)?.guards.some((guard) => guard.mark === mark);
|
|
350
|
+
}
|
|
351
|
+
function guardField(config, guard) {
|
|
352
|
+
const previous = intactGuards(config);
|
|
353
|
+
const base = previous ? { resolve: previous.resolve, subscribe: previous.subscribe } : {
|
|
354
|
+
resolve: config.resolve ?? defaultFieldResolver,
|
|
355
|
+
subscribe: config.subscribe
|
|
356
|
+
};
|
|
357
|
+
const guards = [...previous?.guards ?? [], guard].sort((a, b) => a.rank - b.rank);
|
|
358
|
+
const wrapped = {
|
|
359
|
+
resolve: checked(guards, base.resolve),
|
|
360
|
+
subscribe: base.subscribe && checked(guards.filter((one) => one.onSubscribe), base.subscribe)
|
|
361
|
+
};
|
|
341
362
|
return {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
363
|
+
...config,
|
|
364
|
+
extensions: {
|
|
365
|
+
...config.extensions,
|
|
366
|
+
[GUARDS]: { guards, ...base, wrapped }
|
|
345
367
|
},
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
368
|
+
resolve: wrapped.resolve,
|
|
369
|
+
subscribe: wrapped.subscribe
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function checked(guards, next) {
|
|
373
|
+
return async (source, args, context, info) => {
|
|
374
|
+
for (const { check } of guards)
|
|
375
|
+
await check(source, args, context);
|
|
376
|
+
return next(source, args, context, info);
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/plugins/apply-authenticated.ts
|
|
381
|
+
var MARK = "@nxgt/shared-graphql:authenticated";
|
|
382
|
+
function applyAuthenticated(schema, options = {}) {
|
|
383
|
+
const known = options.types ?? CALLER_TYPES;
|
|
384
|
+
let guardedNow = 0;
|
|
385
|
+
const mapped = mapSchema(schema, {
|
|
386
|
+
[MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
|
|
387
|
+
const where = `${typeName}.${fieldName}`;
|
|
388
|
+
const requirement = requirementOf(schema, fieldConfig, where, known);
|
|
389
|
+
if (!requirement || isGuarded(fieldConfig, MARK))
|
|
390
|
+
return fieldConfig;
|
|
391
|
+
guardedNow += 1;
|
|
392
|
+
return guarded(fieldConfig, requirement);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
return guardedNow > 0 ? mapped : schema;
|
|
396
|
+
}
|
|
397
|
+
function requirementOf(schema, fieldConfig, where, known) {
|
|
398
|
+
const [typeName = "", fieldName = ""] = where.split(".");
|
|
399
|
+
const type = schema.getType(typeName);
|
|
400
|
+
const interfaces = isObjectType(type) ? type.getInterfaces() : [];
|
|
401
|
+
const read = (node, at) => readAuthenticated(schema, node, at, known);
|
|
402
|
+
return combineRequirements([
|
|
403
|
+
read(fieldConfig, where),
|
|
404
|
+
type ? read(type, typeName) : null,
|
|
405
|
+
...interfaces.flatMap((iface) => {
|
|
406
|
+
const field = iface.getFields()[fieldName];
|
|
407
|
+
return [
|
|
408
|
+
read(iface, iface.name),
|
|
409
|
+
field ? read(field, `${iface.name}.${fieldName}`) : null
|
|
410
|
+
];
|
|
411
|
+
}),
|
|
412
|
+
leafRequirement(schema, fieldConfig, read)
|
|
413
|
+
], where);
|
|
414
|
+
}
|
|
415
|
+
function leafRequirement(schema, fieldConfig, read) {
|
|
416
|
+
const leaf = schema.getType(getNamedType(fieldConfig.type).name);
|
|
417
|
+
if (!leaf || !(isScalarType(leaf) || isEnumType(leaf)))
|
|
418
|
+
return null;
|
|
419
|
+
return read(leaf, leaf.name);
|
|
420
|
+
}
|
|
421
|
+
function guarded(fieldConfig, { types }) {
|
|
422
|
+
return guardField(fieldConfig, {
|
|
423
|
+
mark: MARK,
|
|
424
|
+
rank: 0,
|
|
425
|
+
onSubscribe: true,
|
|
426
|
+
check: (_source, _args, context) => {
|
|
427
|
+
const ctx = context ?? {};
|
|
428
|
+
if (!ctx.user)
|
|
429
|
+
throw denial(ErrorCode2.Unauthenticated);
|
|
430
|
+
if (types && !types.includes(callerTypeOf(ctx) ?? "")) {
|
|
431
|
+
throw denial(ErrorCode2.Forbidden);
|
|
354
432
|
}
|
|
355
433
|
}
|
|
356
|
-
};
|
|
434
|
+
});
|
|
357
435
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
ID: "any",
|
|
361
|
-
DateTime: "any",
|
|
362
|
-
Date: "any",
|
|
363
|
-
Any: "any",
|
|
364
|
-
BigInt: "number",
|
|
365
|
-
EmailAddress: "string",
|
|
366
|
-
GeoJSON: "any",
|
|
367
|
-
HSL: "string",
|
|
368
|
-
HSLA: "string",
|
|
369
|
-
HexColorCode: "string",
|
|
370
|
-
Hexadecimal: "string",
|
|
371
|
-
IBAN: "string",
|
|
372
|
-
IP: "string",
|
|
373
|
-
Time: "string",
|
|
374
|
-
LocalTime: "string",
|
|
375
|
-
UUID: "string",
|
|
376
|
-
PositiveInt: "number",
|
|
377
|
-
NegativeInt: "number",
|
|
378
|
-
NonPositiveInt: "number",
|
|
379
|
-
NonNegativeInt: "number",
|
|
380
|
-
PositiveFloat: "number",
|
|
381
|
-
NegativeFloat: "number",
|
|
382
|
-
NonPositiveFloat: "number",
|
|
383
|
-
NonNegativeFloat: "number",
|
|
384
|
-
NonEmptyString: "string",
|
|
385
|
-
PostalCode: "string",
|
|
386
|
-
RGB: "string",
|
|
387
|
-
RGBA: "string",
|
|
388
|
-
SESSN: "string",
|
|
389
|
-
Timestamp: "string",
|
|
390
|
-
URL: "string",
|
|
391
|
-
USCurrency: "string",
|
|
392
|
-
UTCOffset: "string",
|
|
393
|
-
Long: "number",
|
|
394
|
-
Short: "number",
|
|
395
|
-
Byte: "number",
|
|
396
|
-
BigDecimal: "string",
|
|
397
|
-
BigInteger: "string",
|
|
398
|
-
Char: "string",
|
|
399
|
-
Locale: "string",
|
|
400
|
-
CountryCode: "string",
|
|
401
|
-
CountryName: "string",
|
|
402
|
-
AccountNumber: "string",
|
|
403
|
-
CurrencyCode: "string",
|
|
404
|
-
Duration: "string",
|
|
405
|
-
Url: "string",
|
|
406
|
-
JSONObject: "Record<string, any>",
|
|
407
|
-
JSON: "any",
|
|
408
|
-
Void: "void",
|
|
409
|
-
ObjectID: "any",
|
|
410
|
-
Upload: "File | Blob",
|
|
411
|
-
Email: "string",
|
|
412
|
-
Port: "number",
|
|
413
|
-
IPv4: "string",
|
|
414
|
-
IPv6: "string",
|
|
415
|
-
MAC: "string",
|
|
416
|
-
Password: "string",
|
|
417
|
-
PhoneNumber: "string",
|
|
418
|
-
ZIPCode: "string",
|
|
419
|
-
SSN: "string",
|
|
420
|
-
GUID: "string",
|
|
421
|
-
ISBN: "string",
|
|
422
|
-
Latitude: "number",
|
|
423
|
-
Longitude: "number",
|
|
424
|
-
JWT: "string"
|
|
425
|
-
};
|
|
426
|
-
// src/utils/data-loader.utils.ts
|
|
427
|
-
import { cast } from "@nxgt/shared/helpers";
|
|
428
|
-
import {
|
|
429
|
-
paginateList
|
|
430
|
-
} from "@nxgt/shared-mongo";
|
|
431
|
-
import DataLoader from "dataloader";
|
|
432
|
-
import { isArray } from "lodash";
|
|
433
|
-
function createDataLoader(batchLoadFn, options) {
|
|
434
|
-
return new DataLoader(batchLoadFn, options);
|
|
435
|
-
}
|
|
436
|
-
function createConnectionListDataLoader(path, model, options) {
|
|
437
|
-
return createDataLoader(async (docs) => {
|
|
438
|
-
const data = await model.find({
|
|
439
|
-
...options.filter ?? {},
|
|
440
|
-
_id: docs.flatMap((doc) => {
|
|
441
|
-
const ids = doc[path] ?? [];
|
|
442
|
-
return isArray(ids) ? cast(ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))) : [];
|
|
443
|
-
})
|
|
444
|
-
}).exec();
|
|
445
|
-
return cast(docs.map((doc) => model.paginateList(data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString())), options)));
|
|
446
|
-
});
|
|
436
|
+
function callerTypeOf(ctx) {
|
|
437
|
+
return ctx.ory?.kind ?? ctx.user?.tokenType;
|
|
447
438
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
439
|
+
// src/plugins/apply-keto-checks.ts
|
|
440
|
+
import { MapperKind as MapperKind2, mapSchema as mapSchema2 } from "@graphql-tools/utils";
|
|
441
|
+
import { evaluateRequirement } from "@nxgt/ory-sdk";
|
|
442
|
+
import { ErrorCode as ErrorCode3 } from "@nxgt/shared-exceptions";
|
|
443
|
+
|
|
444
|
+
// src/directives/removed-check.ts
|
|
445
|
+
import { getDirective as getDirective3 } from "@graphql-tools/utils";
|
|
446
|
+
function refuseRemovedCheck(schema, node, where) {
|
|
447
|
+
const declared = schema.getDirective("check");
|
|
448
|
+
if (!declared?.args.some((arg) => arg.name === "permissions"))
|
|
449
|
+
return;
|
|
450
|
+
if (!getDirective3(schema, node, "check")?.length)
|
|
451
|
+
return;
|
|
452
|
+
throw new TypeError(`@check on ${where}: @check was removed in @nxgt/shared-graphql 3.0 — write one @permission(name: "<permit>", type: "<namespace>") per term, and move an OR into the Keto model`);
|
|
455
453
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
return result.slice(0, options.limit);
|
|
473
|
-
}
|
|
474
|
-
if (options?.skip) {
|
|
475
|
-
return result.slice(options.skip);
|
|
454
|
+
|
|
455
|
+
// src/plugins/apply-keto-checks.ts
|
|
456
|
+
var MARK2 = "@nxgt/shared-graphql:keto-checks";
|
|
457
|
+
function applyKetoChecks(schema, options = {}) {
|
|
458
|
+
let guardedNow = 0;
|
|
459
|
+
const mapped = mapSchema2(schema, {
|
|
460
|
+
[MapperKind2.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => {
|
|
461
|
+
refuseOnInterface(schema, fieldConfig, `${typeName}.${fieldName}`);
|
|
462
|
+
return fieldConfig;
|
|
463
|
+
},
|
|
464
|
+
[MapperKind2.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => {
|
|
465
|
+
const where = `${typeName}.${fieldName}`;
|
|
466
|
+
refuseRemovedCheck(schema, fieldConfig, where);
|
|
467
|
+
const requirements = readPermissions(schema, fieldConfig, where, options);
|
|
468
|
+
if (requirements.length === 0 || isGuarded(fieldConfig, MARK2)) {
|
|
469
|
+
return fieldConfig;
|
|
476
470
|
}
|
|
477
|
-
|
|
478
|
-
|
|
471
|
+
guardedNow += 1;
|
|
472
|
+
return guarded2(fieldConfig, requirements, where);
|
|
473
|
+
}
|
|
479
474
|
});
|
|
475
|
+
return guardedNow > 0 ? mapped : schema;
|
|
480
476
|
}
|
|
481
|
-
function
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
477
|
+
function refuseOnInterface(schema, fieldConfig, where) {
|
|
478
|
+
refuseRemovedCheck(schema, fieldConfig, where);
|
|
479
|
+
if (readPermissions(schema, fieldConfig, where).length > 0) {
|
|
480
|
+
throw new TypeError(`${where}: @permission on an interface field guards nothing — no resolver runs there. Put it on each implementing type's field`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function guarded2(fieldConfig, requirements, where) {
|
|
484
|
+
return guardField(fieldConfig, {
|
|
485
|
+
mark: MARK2,
|
|
486
|
+
rank: 1,
|
|
487
|
+
onSubscribe: requirements.every(({ permissions }) => permissions.every((group) => group.every((term) => term.id.startsWith("args.")))),
|
|
488
|
+
check: async (source, args, context) => {
|
|
489
|
+
const ctx = context;
|
|
490
|
+
const subject = ctx.ory?.subject;
|
|
491
|
+
if (!subject)
|
|
492
|
+
throw denial(ErrorCode3.Unauthenticated);
|
|
493
|
+
const check = ctx.ketoChecks;
|
|
494
|
+
if (!check) {
|
|
495
|
+
throw new Error(`${where}: no checker on the context — useKetoChecks(ory) is not registered`);
|
|
495
496
|
}
|
|
496
|
-
|
|
497
|
-
|
|
497
|
+
for (const { permissions, onDeny, message } of requirements) {
|
|
498
|
+
const allowed = await evaluateRequirement(permissions, (term) => objectsOf(term, source, args, where), check, subject);
|
|
499
|
+
if (!allowed)
|
|
500
|
+
throw refusal(onDeny, message);
|
|
498
501
|
}
|
|
499
|
-
|
|
500
|
-
}));
|
|
502
|
+
}
|
|
501
503
|
});
|
|
502
504
|
}
|
|
503
|
-
function
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
505
|
+
function objectsOf(term, source, args, where) {
|
|
506
|
+
const ids = objectIds(readPath(term.id, source, args));
|
|
507
|
+
if (ids.length === 0) {
|
|
508
|
+
throw new Error(`${where}: "${term.id}" resolved no object id`);
|
|
509
|
+
}
|
|
510
|
+
return ids;
|
|
511
|
+
}
|
|
512
|
+
function refusal(onDeny, message) {
|
|
513
|
+
return denial(onDeny === "FORBIDDEN" ? ErrorCode3.Forbidden : ErrorCode3.NotFound, message);
|
|
514
|
+
}
|
|
515
|
+
// src/plugins/gateway-trust.ts
|
|
516
|
+
import {
|
|
517
|
+
requireGatewayTrust as requireTrust
|
|
518
|
+
} from "@nxgt/security/gateway";
|
|
519
|
+
import {
|
|
520
|
+
GATEWAY_SECRET_HEADER,
|
|
521
|
+
gatewaySecret
|
|
522
|
+
} from "@nxgt/security/gateway";
|
|
523
|
+
function requireGatewayTrust(options, plugin) {
|
|
524
|
+
return requireTrust(options, {
|
|
525
|
+
caller: plugin,
|
|
526
|
+
reads: "the request's extensions",
|
|
527
|
+
alternative: "useOryAuth(ory)"
|
|
510
528
|
});
|
|
511
529
|
}
|
|
512
|
-
function
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
530
|
+
function principalOf(value) {
|
|
531
|
+
if (!value || typeof value !== "object")
|
|
532
|
+
return;
|
|
533
|
+
const sub = value.sub;
|
|
534
|
+
return typeof sub === "string" && sub.length > 0 ? value : undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/plugins/auth.ts
|
|
538
|
+
function useAuth(options) {
|
|
539
|
+
const trusted = requireGatewayTrust(options, "useAuth()");
|
|
540
|
+
return {
|
|
541
|
+
onContextBuilding: async ({ context, extendContext }) => {
|
|
542
|
+
const headers = context.request?.headers;
|
|
543
|
+
if (typeof headers?.get !== "function")
|
|
544
|
+
return;
|
|
545
|
+
if (!await trusted(headers))
|
|
546
|
+
return;
|
|
547
|
+
const user = principalOf(context.params?.extensions?.user);
|
|
548
|
+
if (!user)
|
|
549
|
+
return;
|
|
550
|
+
const token = context.params.extensions?.token;
|
|
551
|
+
extendContext({
|
|
552
|
+
user,
|
|
553
|
+
token: typeof token === "string" ? token : undefined
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
// src/plugins/authenticated.ts
|
|
559
|
+
function useAuthenticated(options = {}) {
|
|
560
|
+
return {
|
|
561
|
+
onSchemaChange: ({ schema, replaceSchema }) => {
|
|
562
|
+
const next = applyAuthenticated(schema, options);
|
|
563
|
+
if (next !== schema)
|
|
564
|
+
replaceSchema(next);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
// src/plugins/extract-jwt.ts
|
|
569
|
+
function extractJwtPlugin(options) {
|
|
570
|
+
const trusted = requireGatewayTrust(options, "extractJwtPlugin()");
|
|
571
|
+
return {
|
|
572
|
+
async requestDidStart({ request, contextValue }) {
|
|
573
|
+
const headers = request.http?.headers;
|
|
574
|
+
if (!headers || !await trusted(headers))
|
|
575
|
+
return;
|
|
576
|
+
const payload = principalOf(request.extensions?.payload);
|
|
577
|
+
if (payload)
|
|
578
|
+
contextValue.jwt = { payload };
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
// src/plugins/keto-checker.ts
|
|
583
|
+
import { tuple } from "@nxgt/ory-sdk";
|
|
584
|
+
import DataLoader from "dataloader";
|
|
585
|
+
function createKetoChecks(ory) {
|
|
586
|
+
const loader = new DataLoader((questions) => ory.checkMany([...questions]), {
|
|
587
|
+
cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
|
|
519
588
|
});
|
|
589
|
+
return (permission, subject) => loader.load({ permission, subject });
|
|
520
590
|
}
|
|
521
|
-
// src/
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
591
|
+
// src/plugins/keto-checks.ts
|
|
592
|
+
function useKetoChecks(ory, options = {}) {
|
|
593
|
+
return {
|
|
594
|
+
onSchemaChange: ({ schema, replaceSchema }) => {
|
|
595
|
+
const next = applyKetoChecks(schema, options);
|
|
596
|
+
if (next !== schema)
|
|
597
|
+
replaceSchema(next);
|
|
598
|
+
},
|
|
599
|
+
onContextBuilding: ({ extendContext }) => {
|
|
600
|
+
extendContext({ ketoChecks: createKetoChecks(ory) });
|
|
601
|
+
}
|
|
602
|
+
};
|
|
525
603
|
}
|
|
526
|
-
// src/
|
|
527
|
-
import {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
604
|
+
// src/plugins/keto-helpers.ts
|
|
605
|
+
import { ErrorCode as ErrorCode4 } from "@nxgt/shared-exceptions";
|
|
606
|
+
function requireUser(context) {
|
|
607
|
+
if (!context.user) {
|
|
608
|
+
throw denial(ErrorCode4.Unauthenticated);
|
|
609
|
+
}
|
|
610
|
+
return context.user;
|
|
611
|
+
}
|
|
612
|
+
async function can(context, question) {
|
|
613
|
+
const subject = context.ory?.subject;
|
|
614
|
+
if (!subject) {
|
|
615
|
+
throw denial(ErrorCode4.Unauthenticated);
|
|
616
|
+
}
|
|
617
|
+
if (!context.ketoChecks) {
|
|
618
|
+
throw new Error("can(): no checker on the context — useKetoChecks(ory) is not registered");
|
|
619
|
+
}
|
|
620
|
+
const permission = {
|
|
621
|
+
namespace: question.type,
|
|
622
|
+
object: question.id,
|
|
623
|
+
relation: question.name
|
|
624
|
+
};
|
|
625
|
+
return context.ketoChecks(permission, subject);
|
|
626
|
+
}
|
|
627
|
+
// src/plugins/ory-auth.ts
|
|
531
628
|
import {
|
|
532
|
-
|
|
533
|
-
} from "@nxgt/
|
|
534
|
-
import {
|
|
535
|
-
import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
|
|
536
|
-
import { kebabCase } from "lodash";
|
|
629
|
+
bearerOf
|
|
630
|
+
} from "@nxgt/ory-sdk";
|
|
631
|
+
import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
|
|
537
632
|
|
|
538
633
|
// src/utils/errors/ory-unavailable.ts
|
|
539
|
-
import { ErrorCode } from "@nxgt/shared-exceptions";
|
|
634
|
+
import { ErrorCode as ErrorCode5 } from "@nxgt/shared-exceptions";
|
|
540
635
|
import { GraphQLError as GraphQLError2 } from "graphql";
|
|
541
636
|
function isOryUnavailable(error) {
|
|
542
637
|
if (!(error instanceof Error))
|
|
@@ -547,598 +642,192 @@ function isOryUnavailable(error) {
|
|
|
547
642
|
function serviceUnavailableError(error) {
|
|
548
643
|
return new GraphQLError2(`ory: ${error.service} is unavailable`, {
|
|
549
644
|
extensions: {
|
|
550
|
-
code:
|
|
645
|
+
code: ErrorCode5.ServiceUnavailable,
|
|
551
646
|
http: { status: 503 },
|
|
552
647
|
debugMessage: error.message
|
|
553
648
|
}
|
|
554
649
|
});
|
|
555
650
|
}
|
|
556
651
|
|
|
557
|
-
// src/
|
|
558
|
-
function
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
if (error instanceof CustomException || error instanceof mongoose.MongooseError) {
|
|
573
|
-
const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
|
|
574
|
-
return {
|
|
575
|
-
...formattedError,
|
|
576
|
-
message: exception.errorCode === ErrorCode2.ValidationError ? exception.message : translate(exception.message, exception.options),
|
|
577
|
-
extensions: {
|
|
578
|
-
...formattedError.extensions,
|
|
579
|
-
code: exception.errorCode,
|
|
580
|
-
debugMessage: exception.debugMessage
|
|
581
|
-
}
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
if ([
|
|
585
|
-
ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED,
|
|
586
|
-
ApolloServerErrorCode.BAD_REQUEST,
|
|
587
|
-
ApolloServerErrorCode.BAD_USER_INPUT,
|
|
588
|
-
ApolloServerErrorCode.GRAPHQL_PARSE_FAILED,
|
|
589
|
-
ApolloServerErrorCode.OPERATION_RESOLUTION_FAILURE,
|
|
590
|
-
ApolloServerErrorCode.PERSISTED_QUERY_NOT_FOUND,
|
|
591
|
-
ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED
|
|
592
|
-
].includes(formattedError.extensions?.code)) {
|
|
593
|
-
return {
|
|
594
|
-
...formattedError,
|
|
595
|
-
message: translate(`errors.${kebabCase(formattedError.extensions?.code)}`, {}),
|
|
596
|
-
extensions: {
|
|
597
|
-
...formattedError.extensions,
|
|
598
|
-
debugMessage: formattedError.message
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
return formattedError;
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
// src/utils/errors/mask-error.ts
|
|
606
|
-
import {
|
|
607
|
-
translate as translateBase2
|
|
608
|
-
} from "@nxgt/i18n";
|
|
609
|
-
import { CustomException as CustomException2, ErrorCode as ErrorCode3 } from "@nxgt/shared-exceptions";
|
|
610
|
-
import { MONGO_UTILS as MONGO_UTILS2, mongoose as mongoose2 } from "@nxgt/shared-mongo";
|
|
611
|
-
import { GraphQLError as GraphQLError3 } from "graphql";
|
|
612
|
-
function createMaskError(translate = translateBase2) {
|
|
613
|
-
return (error, message, isDev) => {
|
|
614
|
-
const original = error instanceof GraphQLError3 && error.originalError ? error.originalError : error;
|
|
615
|
-
if (isOryUnavailable(original))
|
|
616
|
-
return serviceUnavailableError(original);
|
|
617
|
-
if (original instanceof CustomException2 || original instanceof mongoose2.MongooseError) {
|
|
618
|
-
return exceptionError(original, translate);
|
|
619
|
-
}
|
|
620
|
-
if (isOriginalGraphQLError(error))
|
|
621
|
-
return error;
|
|
622
|
-
return new GraphQLError3(message, {
|
|
623
|
-
...locationOf(error),
|
|
624
|
-
extensions: isDev ? {
|
|
625
|
-
code: ErrorCode3.InternalServerError,
|
|
626
|
-
debugMessage: String(original)
|
|
627
|
-
} : { code: ErrorCode3.InternalServerError }
|
|
628
|
-
});
|
|
652
|
+
// src/plugins/ory-auth.ts
|
|
653
|
+
function toPrincipal(ory) {
|
|
654
|
+
const email = ory.identity?.email;
|
|
655
|
+
return {
|
|
656
|
+
sub: ory.subject,
|
|
657
|
+
uid: ory.subject,
|
|
658
|
+
name: email ?? ory.clientId ?? ory.subject,
|
|
659
|
+
username: email,
|
|
660
|
+
clientId: ory.clientId,
|
|
661
|
+
scope: ory.scopes.join(" ") || undefined,
|
|
662
|
+
tokenType: ory.kind,
|
|
663
|
+
exp: ory.expiresAt ? Math.floor(ory.expiresAt.getTime() / 1000) : undefined,
|
|
664
|
+
authorities: [],
|
|
665
|
+
roles: []
|
|
629
666
|
};
|
|
630
667
|
}
|
|
631
|
-
function
|
|
632
|
-
|
|
633
|
-
const status = exception.code;
|
|
634
|
-
return new GraphQLError3(exception.errorCode === ErrorCode3.ValidationError ? exception.message : translate(exception.message, exception.options), {
|
|
635
|
-
extensions: {
|
|
636
|
-
code: exception.errorCode,
|
|
637
|
-
...status ? { http: { status } } : {},
|
|
638
|
-
...exception.debugMessage ? { debugMessage: exception.debugMessage } : {}
|
|
639
|
-
}
|
|
640
|
-
});
|
|
668
|
+
function oryUnavailableError(error) {
|
|
669
|
+
return serviceUnavailableError(error);
|
|
641
670
|
}
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
671
|
+
async function resolveOryPrincipal(ory, headers) {
|
|
672
|
+
let principal;
|
|
673
|
+
try {
|
|
674
|
+
principal = await ory.resolve(headers);
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (isOryUnavailable(error))
|
|
677
|
+
throw oryUnavailableError(error);
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
ory: principal,
|
|
682
|
+
user: principal ? toPrincipal(principal) : undefined,
|
|
683
|
+
claims: principal ? claimsFromOryPrincipal(principal) : undefined,
|
|
684
|
+
token: bearerOf(headers) ?? undefined
|
|
685
|
+
};
|
|
646
686
|
}
|
|
647
|
-
function
|
|
648
|
-
if (!(error instanceof GraphQLError3))
|
|
649
|
-
return {};
|
|
687
|
+
function useOryAuth(ory) {
|
|
650
688
|
return {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
path: error.path
|
|
689
|
+
onContextBuilding: async ({ context, extendContext }) => {
|
|
690
|
+
extendContext(await resolveOryPrincipal(ory, context.request.headers));
|
|
691
|
+
}
|
|
655
692
|
};
|
|
656
693
|
}
|
|
657
|
-
// src/
|
|
658
|
-
import {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
694
|
+
// src/scalars/any/any.scalars.ts
|
|
695
|
+
import { GraphQLScalarType, Kind } from "graphql";
|
|
696
|
+
var ANY_SCALAR = {
|
|
697
|
+
Any: new GraphQLScalarType({
|
|
698
|
+
name: "Any",
|
|
699
|
+
description: "Generic scalar to represent any type",
|
|
700
|
+
serialize(value) {
|
|
701
|
+
return value;
|
|
702
|
+
},
|
|
703
|
+
parseValue(value) {
|
|
704
|
+
return value;
|
|
705
|
+
},
|
|
706
|
+
parseLiteral(ast, variables) {
|
|
707
|
+
switch (ast.kind) {
|
|
708
|
+
case Kind.STRING:
|
|
709
|
+
case Kind.BOOLEAN:
|
|
710
|
+
return ast.value;
|
|
711
|
+
case Kind.INT:
|
|
712
|
+
case Kind.FLOAT:
|
|
713
|
+
return parseFloat(ast.value);
|
|
714
|
+
case Kind.OBJECT:
|
|
715
|
+
return Object.fromEntries(ast.fields.map((field) => [field.name, field.value]));
|
|
716
|
+
case Kind.LIST:
|
|
717
|
+
return ast.values.map((n) => this?.parseLiteral?.(n, variables));
|
|
718
|
+
case Kind.NULL:
|
|
719
|
+
return null;
|
|
720
|
+
case Kind.VARIABLE: {
|
|
721
|
+
const name = ast.name.value;
|
|
722
|
+
return variables ? variables[name] : undefined;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
})
|
|
669
727
|
};
|
|
670
|
-
// src/
|
|
671
|
-
import {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
728
|
+
// src/scalars/custom/custom.scalars.ts
|
|
729
|
+
import {
|
|
730
|
+
AccountNumberResolver,
|
|
731
|
+
BigIntResolver,
|
|
732
|
+
CountryCodeResolver,
|
|
733
|
+
CountryNameResolver,
|
|
734
|
+
CuidResolver,
|
|
735
|
+
CurrencyResolver,
|
|
736
|
+
DateResolver,
|
|
737
|
+
DateTimeISOResolver,
|
|
738
|
+
DurationResolver,
|
|
739
|
+
EmailAddressResolver,
|
|
740
|
+
GeoJSONResolver,
|
|
741
|
+
GUIDResolver,
|
|
742
|
+
HexadecimalResolver,
|
|
743
|
+
HexColorCodeResolver,
|
|
744
|
+
HSLAResolver,
|
|
745
|
+
HSLResolver,
|
|
746
|
+
IBANResolver,
|
|
747
|
+
IPResolver,
|
|
748
|
+
IPv4Resolver,
|
|
749
|
+
IPv6Resolver,
|
|
750
|
+
ISBNResolver,
|
|
751
|
+
JSONObjectResolver,
|
|
752
|
+
JSONResolver,
|
|
753
|
+
JWTResolver,
|
|
754
|
+
LatitudeResolver,
|
|
755
|
+
LocaleResolver,
|
|
756
|
+
LongitudeResolver,
|
|
757
|
+
LongResolver,
|
|
758
|
+
MACResolver,
|
|
759
|
+
NegativeFloatResolver,
|
|
760
|
+
NegativeIntResolver,
|
|
761
|
+
NonEmptyStringResolver,
|
|
762
|
+
NonNegativeFloatResolver,
|
|
763
|
+
NonNegativeIntResolver,
|
|
764
|
+
NonPositiveFloatResolver,
|
|
765
|
+
NonPositiveIntResolver,
|
|
766
|
+
ObjectIDResolver,
|
|
767
|
+
PhoneNumberResolver,
|
|
768
|
+
PortResolver,
|
|
769
|
+
PositiveFloatResolver,
|
|
770
|
+
PositiveIntResolver,
|
|
771
|
+
PostalCodeResolver,
|
|
772
|
+
RGBAResolver,
|
|
773
|
+
RGBResolver,
|
|
774
|
+
SESSNResolver,
|
|
775
|
+
TimeResolver,
|
|
776
|
+
TimestampResolver,
|
|
777
|
+
URLResolver,
|
|
778
|
+
USCurrencyResolver,
|
|
779
|
+
UtcOffsetResolver,
|
|
780
|
+
UUIDResolver,
|
|
781
|
+
VoidResolver
|
|
782
|
+
} from "graphql-scalars";
|
|
700
783
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
import {
|
|
704
|
-
import {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
import { pruneSchema } from "@graphql-tools/utils";
|
|
710
|
-
import { printSchema } from "graphql";
|
|
711
|
-
function packageRoot() {
|
|
712
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
713
|
-
while (!existsSync(join(dir, "package.json"))) {
|
|
714
|
-
const parent = dirname(dir);
|
|
715
|
-
if (parent === dir)
|
|
716
|
-
throw new Error("@nxgt/shared-graphql: no package root");
|
|
717
|
-
dir = parent;
|
|
718
|
-
}
|
|
719
|
-
return dir;
|
|
720
|
-
}
|
|
721
|
-
var SHARED_SCHEMA_PATH = join(packageRoot(), "graphql/**/*.graphqls");
|
|
722
|
-
function loadTypeDefs(...paths) {
|
|
723
|
-
const typeDefs = mergeTypeDefs([
|
|
724
|
-
...paths.flatMap((path) => loadFilesSync(path, {
|
|
725
|
-
recursive: true
|
|
726
|
-
}))
|
|
727
|
-
]);
|
|
728
|
-
return typeDefs;
|
|
729
|
-
}
|
|
730
|
-
function buildSubgraphSchema(modulesOrSdl) {
|
|
731
|
-
return pruneSchema(buildSubgraphSchemaBase(modulesOrSdl), {
|
|
732
|
-
skipUnusedTypesPruning: false
|
|
733
|
-
});
|
|
734
|
-
}
|
|
735
|
-
async function generateSchema(output, ...paths) {
|
|
736
|
-
await Bun.write(output, printSchema(buildSubgraphSchema(loadTypeDefs(...paths))));
|
|
737
|
-
}
|
|
738
|
-
// src/utils/websocket.ts
|
|
739
|
-
import { createServer } from "node:http";
|
|
740
|
-
import { useServer } from "graphql-ws/use/ws";
|
|
741
|
-
import { WebSocketServer } from "ws";
|
|
742
|
-
function setupYogaWebSocketServer({
|
|
743
|
-
yoga,
|
|
744
|
-
context
|
|
784
|
+
// src/scalars/custom/utils.ts
|
|
785
|
+
import { CustomException } from "@nxgt/shared-exceptions";
|
|
786
|
+
import { logger } from "@nxgt/shared-logging";
|
|
787
|
+
import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
|
|
788
|
+
function createScalarFrom(scalar, {
|
|
789
|
+
name,
|
|
790
|
+
description,
|
|
791
|
+
errorMessage
|
|
745
792
|
}) {
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
server: httpServer,
|
|
749
|
-
path: yoga.graphqlEndpoint
|
|
750
|
-
});
|
|
751
|
-
useServer({
|
|
752
|
-
execute: (args) => args.rootValue.execute(args),
|
|
753
|
-
subscribe: (args) => args.rootValue.subscribe(args),
|
|
754
|
-
onSubscribe: async (ctx, _id, params) => {
|
|
755
|
-
const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped({
|
|
756
|
-
...ctx,
|
|
757
|
-
req: ctx.extra.request,
|
|
758
|
-
socket: ctx.extra.socket,
|
|
759
|
-
params
|
|
760
|
-
});
|
|
761
|
-
const args = {
|
|
762
|
-
schema,
|
|
763
|
-
operationName: params.operationName,
|
|
764
|
-
document: parse(params.query),
|
|
765
|
-
variableValues: params.variables,
|
|
766
|
-
contextValue: await contextFactory(),
|
|
767
|
-
rootValue: {
|
|
768
|
-
execute,
|
|
769
|
-
subscribe
|
|
770
|
-
}
|
|
771
|
-
};
|
|
772
|
-
const errors = validate(args.schema, args.document);
|
|
773
|
-
if (errors.length)
|
|
774
|
-
return errors;
|
|
775
|
-
return args;
|
|
776
|
-
},
|
|
777
|
-
context
|
|
778
|
-
}, wsServer);
|
|
779
|
-
const startServer = (port) => new Promise((resolve) => httpServer.listen({ port }, resolve));
|
|
780
|
-
return { yoga, httpServer, startServer };
|
|
781
|
-
}
|
|
782
|
-
// src/utils/ws-context.ts
|
|
783
|
-
async function resolveWsUser(connectionParams, authClient) {
|
|
784
|
-
const rawToken = connectionParams?.authorization ?? connectionParams?.Authorization;
|
|
785
|
-
const token = rawToken?.replace("Bearer ", "");
|
|
786
|
-
const { data } = token ? await authClient.POST("/oauth/introspect", { body: { token } }) : { data: null };
|
|
787
|
-
const user = data?.active ? { ...data, name: data.sub } : undefined;
|
|
788
|
-
return { user, token };
|
|
789
|
-
}
|
|
790
|
-
// src/integrations/hono.ts
|
|
791
|
-
function sandboxExpolorer(options) {
|
|
792
|
-
return createMiddleware(async (ctx) => {
|
|
793
|
-
return ctx.html(renderSandbox(options));
|
|
794
|
-
});
|
|
795
|
-
}
|
|
796
|
-
function honoYoga(yoga) {
|
|
797
|
-
return createMiddleware(async (ctx) => {
|
|
798
|
-
const response = await yoga.fetch(ctx.req.raw);
|
|
799
|
-
return response;
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
function createYogaHono(yoga, options) {
|
|
803
|
-
const app = new Hono;
|
|
804
|
-
app.use(contextStorage());
|
|
805
|
-
app.use(languageDetector({
|
|
806
|
-
order: ["header"],
|
|
807
|
-
supportedLanguages: ["en", "fr"],
|
|
808
|
-
fallbackLanguage: "en",
|
|
809
|
-
caches: []
|
|
810
|
-
}));
|
|
811
|
-
app.get("/health", (ctx) => {
|
|
812
|
-
return ctx.json({ status: "ok" }, 200);
|
|
793
|
+
const exception = CustomException.badRequest({
|
|
794
|
+
message: errorMessage
|
|
813
795
|
});
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
refuseOnInterface(schema, fieldConfig, `${typeName}.${fieldName}`);
|
|
829
|
-
return fieldConfig;
|
|
796
|
+
return new GraphQLScalarType2({
|
|
797
|
+
...scalar,
|
|
798
|
+
name: name ?? scalar.name,
|
|
799
|
+
description: (description ?? scalar.description)?.replaceAll("`", "'"),
|
|
800
|
+
serialize(value) {
|
|
801
|
+
if (!errorMessage) {
|
|
802
|
+
return scalar.serialize(value);
|
|
803
|
+
}
|
|
804
|
+
try {
|
|
805
|
+
return scalar.serialize(value);
|
|
806
|
+
} catch (e) {
|
|
807
|
+
logger.error(e);
|
|
808
|
+
throw exception;
|
|
809
|
+
}
|
|
830
810
|
},
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
if (requirements.length === 0)
|
|
835
|
-
return fieldConfig;
|
|
836
|
-
return guarded(fieldConfig, requirements, where);
|
|
837
|
-
}
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
function refuseOnInterface(schema, fieldConfig, where) {
|
|
841
|
-
const message = `${where}: @check / @permission on an interface field guards nothing — no resolver runs there. Put it on each implementing type's field`;
|
|
842
|
-
if (readPermissions(schema, fieldConfig, where).length > 0) {
|
|
843
|
-
throw new TypeError(message);
|
|
844
|
-
}
|
|
845
|
-
if (getDirective3(schema, fieldConfig, CHECK_DIRECTIVE_NAME)?.length) {
|
|
846
|
-
warnCheckMistake(message);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
function guarded(fieldConfig, requirements, where) {
|
|
850
|
-
const resolve = fieldConfig.resolve ?? defaultFieldResolver;
|
|
851
|
-
return {
|
|
852
|
-
...fieldConfig,
|
|
853
|
-
resolve: async (source, args, context, info) => {
|
|
854
|
-
const ctx = context;
|
|
855
|
-
const subject = ctx.ory?.subject;
|
|
856
|
-
if (!subject) {
|
|
857
|
-
throw CustomException3.unauthenticated({
|
|
858
|
-
message: "errors.unauthenticated"
|
|
859
|
-
});
|
|
811
|
+
parseValue(value) {
|
|
812
|
+
if (!errorMessage) {
|
|
813
|
+
return scalar.parseValue(value);
|
|
860
814
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
815
|
+
try {
|
|
816
|
+
return scalar.parseValue(value);
|
|
817
|
+
} catch (e) {
|
|
818
|
+
logger.error(e);
|
|
819
|
+
throw exception;
|
|
864
820
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const ids = objectIds(readPath(term.id, source, args));
|
|
876
|
-
if (ids.length === 0) {
|
|
877
|
-
throw new Error(`${where}: "${term.id}" resolved no object id`);
|
|
878
|
-
}
|
|
879
|
-
return ids;
|
|
880
|
-
}
|
|
881
|
-
function denial(onDeny, message) {
|
|
882
|
-
return onDeny === "FORBIDDEN" ? CustomException3.forbidden({
|
|
883
|
-
message: message ?? "errors.insufficient-permissions"
|
|
884
|
-
}) : CustomException3.notFound({ message: message ?? "errors.not-found" });
|
|
885
|
-
}
|
|
886
|
-
// src/plugins/auth.ts
|
|
887
|
-
function useAuth() {
|
|
888
|
-
return {
|
|
889
|
-
onContextBuilding: async ({ context, extendContext }) => {
|
|
890
|
-
extendContext({
|
|
891
|
-
user: context.params.extensions?.user,
|
|
892
|
-
token: context.params.extensions?.token
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
// src/plugins/extract-jwt.ts
|
|
898
|
-
import { logger as logger2 } from "@nxgt/shared-logging";
|
|
899
|
-
var extractJwtPlugin = {
|
|
900
|
-
async requestDidStart({ request, contextValue }) {
|
|
901
|
-
logger2.info(`[${request.extensions?.subgraphName}] Extracting JWT from request extensions: ${request.extensions?.payload?.sub}`, request.extensions);
|
|
902
|
-
contextValue.jwt = {
|
|
903
|
-
payload: request.extensions?.payload
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
};
|
|
907
|
-
// src/plugins/keto-checker.ts
|
|
908
|
-
import { tuple } from "@nxgt/ory-sdk";
|
|
909
|
-
import DataLoader2 from "dataloader";
|
|
910
|
-
function createKetoChecks(ory) {
|
|
911
|
-
const loader = new DataLoader2((questions) => ory.checkMany([...questions]), {
|
|
912
|
-
cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
|
|
913
|
-
});
|
|
914
|
-
return (permission, subject) => loader.load({ permission, subject });
|
|
915
|
-
}
|
|
916
|
-
// src/plugins/keto-checks.ts
|
|
917
|
-
function useKetoChecks(ory, options = {}) {
|
|
918
|
-
const transformed = new WeakSet;
|
|
919
|
-
return {
|
|
920
|
-
onSchemaChange: ({ schema, replaceSchema }) => {
|
|
921
|
-
if (transformed.has(schema))
|
|
922
|
-
return;
|
|
923
|
-
const next = applyKetoChecks(schema, options);
|
|
924
|
-
transformed.add(next);
|
|
925
|
-
replaceSchema(next);
|
|
926
|
-
},
|
|
927
|
-
onContextBuilding: ({ extendContext }) => {
|
|
928
|
-
extendContext({ ketoChecks: createKetoChecks(ory) });
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
// src/plugins/keto-helpers.ts
|
|
933
|
-
import { CustomException as CustomException4 } from "@nxgt/shared-exceptions";
|
|
934
|
-
function requireUser(context) {
|
|
935
|
-
if (!context.user) {
|
|
936
|
-
throw CustomException4.unauthenticated({
|
|
937
|
-
message: "errors.unauthenticated"
|
|
938
|
-
});
|
|
939
|
-
}
|
|
940
|
-
return context.user;
|
|
941
|
-
}
|
|
942
|
-
async function can(context, question) {
|
|
943
|
-
const subject = context.ory?.subject;
|
|
944
|
-
if (!subject) {
|
|
945
|
-
throw CustomException4.unauthenticated({
|
|
946
|
-
message: "errors.unauthenticated"
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
if (!context.ketoChecks) {
|
|
950
|
-
throw new Error("can(): no checker on the context — useKetoChecks(ory) is not registered");
|
|
951
|
-
}
|
|
952
|
-
const permission = {
|
|
953
|
-
namespace: question.type,
|
|
954
|
-
object: question.id,
|
|
955
|
-
relation: question.name
|
|
956
|
-
};
|
|
957
|
-
return context.ketoChecks(permission, subject);
|
|
958
|
-
}
|
|
959
|
-
// src/plugins/ory-auth.ts
|
|
960
|
-
import {
|
|
961
|
-
bearerOf
|
|
962
|
-
} from "@nxgt/ory-sdk";
|
|
963
|
-
import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
|
|
964
|
-
function toPrincipal(ory) {
|
|
965
|
-
const email = ory.identity?.email;
|
|
966
|
-
return {
|
|
967
|
-
sub: ory.subject,
|
|
968
|
-
uid: ory.subject,
|
|
969
|
-
name: email ?? ory.clientId ?? ory.subject,
|
|
970
|
-
username: email,
|
|
971
|
-
clientId: ory.clientId,
|
|
972
|
-
scope: ory.scopes.join(" ") || undefined,
|
|
973
|
-
tokenType: ory.kind,
|
|
974
|
-
exp: ory.expiresAt ? Math.floor(ory.expiresAt.getTime() / 1000) : undefined,
|
|
975
|
-
authorities: [],
|
|
976
|
-
roles: []
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
function oryUnavailableError(error) {
|
|
980
|
-
return serviceUnavailableError(error);
|
|
981
|
-
}
|
|
982
|
-
async function resolveOryPrincipal(ory, headers) {
|
|
983
|
-
let principal;
|
|
984
|
-
try {
|
|
985
|
-
principal = await ory.resolve(headers);
|
|
986
|
-
} catch (error) {
|
|
987
|
-
if (isOryUnavailable(error))
|
|
988
|
-
throw oryUnavailableError(error);
|
|
989
|
-
throw error;
|
|
990
|
-
}
|
|
991
|
-
return {
|
|
992
|
-
ory: principal,
|
|
993
|
-
user: principal ? toPrincipal(principal) : undefined,
|
|
994
|
-
claims: principal ? claimsFromOryPrincipal(principal) : undefined,
|
|
995
|
-
token: bearerOf(headers) ?? undefined
|
|
996
|
-
};
|
|
997
|
-
}
|
|
998
|
-
function useOryAuth(ory) {
|
|
999
|
-
return {
|
|
1000
|
-
onContextBuilding: async ({ context, extendContext }) => {
|
|
1001
|
-
extendContext(await resolveOryPrincipal(ory, context.request.headers));
|
|
1002
|
-
}
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
// src/scalars/any/any.scalars.ts
|
|
1006
|
-
import { GraphQLScalarType, Kind } from "graphql";
|
|
1007
|
-
var ANY_SCALAR = {
|
|
1008
|
-
Any: new GraphQLScalarType({
|
|
1009
|
-
name: "Any",
|
|
1010
|
-
description: "Generic scalar to represent any type",
|
|
1011
|
-
serialize(value) {
|
|
1012
|
-
return value;
|
|
1013
|
-
},
|
|
1014
|
-
parseValue(value) {
|
|
1015
|
-
return value;
|
|
1016
|
-
},
|
|
1017
|
-
parseLiteral(ast, variables) {
|
|
1018
|
-
switch (ast.kind) {
|
|
1019
|
-
case Kind.STRING:
|
|
1020
|
-
case Kind.BOOLEAN:
|
|
1021
|
-
return ast.value;
|
|
1022
|
-
case Kind.INT:
|
|
1023
|
-
case Kind.FLOAT:
|
|
1024
|
-
return parseFloat(ast.value);
|
|
1025
|
-
case Kind.OBJECT:
|
|
1026
|
-
return Object.fromEntries(ast.fields.map((field) => [field.name, field.value]));
|
|
1027
|
-
case Kind.LIST:
|
|
1028
|
-
return ast.values.map((n) => this?.parseLiteral?.(n, variables));
|
|
1029
|
-
case Kind.NULL:
|
|
1030
|
-
return null;
|
|
1031
|
-
case Kind.VARIABLE: {
|
|
1032
|
-
const name = ast.name.value;
|
|
1033
|
-
return variables ? variables[name] : undefined;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
})
|
|
1038
|
-
};
|
|
1039
|
-
// src/scalars/custom/custom.scalars.ts
|
|
1040
|
-
import {
|
|
1041
|
-
AccountNumberResolver,
|
|
1042
|
-
BigIntResolver,
|
|
1043
|
-
CountryCodeResolver,
|
|
1044
|
-
CountryNameResolver,
|
|
1045
|
-
CuidResolver,
|
|
1046
|
-
CurrencyResolver,
|
|
1047
|
-
DateResolver,
|
|
1048
|
-
DateTimeISOResolver,
|
|
1049
|
-
DurationResolver,
|
|
1050
|
-
EmailAddressResolver,
|
|
1051
|
-
GeoJSONResolver,
|
|
1052
|
-
GUIDResolver,
|
|
1053
|
-
HexadecimalResolver,
|
|
1054
|
-
HexColorCodeResolver,
|
|
1055
|
-
HSLAResolver,
|
|
1056
|
-
HSLResolver,
|
|
1057
|
-
IBANResolver,
|
|
1058
|
-
IPResolver,
|
|
1059
|
-
IPv4Resolver,
|
|
1060
|
-
IPv6Resolver,
|
|
1061
|
-
ISBNResolver,
|
|
1062
|
-
JSONObjectResolver,
|
|
1063
|
-
JSONResolver,
|
|
1064
|
-
JWTResolver,
|
|
1065
|
-
LatitudeResolver,
|
|
1066
|
-
LocaleResolver,
|
|
1067
|
-
LongitudeResolver,
|
|
1068
|
-
LongResolver,
|
|
1069
|
-
MACResolver,
|
|
1070
|
-
NegativeFloatResolver,
|
|
1071
|
-
NegativeIntResolver,
|
|
1072
|
-
NonEmptyStringResolver,
|
|
1073
|
-
NonNegativeFloatResolver,
|
|
1074
|
-
NonNegativeIntResolver,
|
|
1075
|
-
NonPositiveFloatResolver,
|
|
1076
|
-
NonPositiveIntResolver,
|
|
1077
|
-
ObjectIDResolver,
|
|
1078
|
-
PhoneNumberResolver,
|
|
1079
|
-
PortResolver,
|
|
1080
|
-
PositiveFloatResolver,
|
|
1081
|
-
PositiveIntResolver,
|
|
1082
|
-
PostalCodeResolver,
|
|
1083
|
-
RGBAResolver,
|
|
1084
|
-
RGBResolver,
|
|
1085
|
-
SESSNResolver,
|
|
1086
|
-
TimeResolver,
|
|
1087
|
-
TimestampResolver,
|
|
1088
|
-
URLResolver,
|
|
1089
|
-
USCurrencyResolver,
|
|
1090
|
-
UtcOffsetResolver,
|
|
1091
|
-
UUIDResolver,
|
|
1092
|
-
VoidResolver
|
|
1093
|
-
} from "graphql-scalars";
|
|
1094
|
-
|
|
1095
|
-
// src/scalars/custom/utils.ts
|
|
1096
|
-
import { CustomException as CustomException5 } from "@nxgt/shared-exceptions";
|
|
1097
|
-
import { logger as logger3 } from "@nxgt/shared-logging";
|
|
1098
|
-
import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
|
|
1099
|
-
function createScalarFrom(scalar, {
|
|
1100
|
-
name,
|
|
1101
|
-
description,
|
|
1102
|
-
errorMessage
|
|
1103
|
-
}) {
|
|
1104
|
-
const exception = CustomException5.badRequest({
|
|
1105
|
-
message: errorMessage
|
|
1106
|
-
});
|
|
1107
|
-
return new GraphQLScalarType2({
|
|
1108
|
-
...scalar,
|
|
1109
|
-
name: name ?? scalar.name,
|
|
1110
|
-
description: (description ?? scalar.description)?.replaceAll("`", "'"),
|
|
1111
|
-
serialize(value) {
|
|
1112
|
-
if (!errorMessage) {
|
|
1113
|
-
return scalar.serialize(value);
|
|
1114
|
-
}
|
|
1115
|
-
try {
|
|
1116
|
-
return scalar.serialize(value);
|
|
1117
|
-
} catch (e) {
|
|
1118
|
-
logger3.error(e);
|
|
1119
|
-
throw exception;
|
|
1120
|
-
}
|
|
1121
|
-
},
|
|
1122
|
-
parseValue(value) {
|
|
1123
|
-
if (!errorMessage) {
|
|
1124
|
-
return scalar.parseValue(value);
|
|
1125
|
-
}
|
|
1126
|
-
try {
|
|
1127
|
-
return scalar.parseValue(value);
|
|
1128
|
-
} catch (e) {
|
|
1129
|
-
logger3.error(e);
|
|
1130
|
-
throw exception;
|
|
1131
|
-
}
|
|
1132
|
-
},
|
|
1133
|
-
parseLiteral(ast, variables) {
|
|
1134
|
-
if (!errorMessage) {
|
|
1135
|
-
return scalar.parseLiteral(ast, variables);
|
|
1136
|
-
}
|
|
1137
|
-
try {
|
|
1138
|
-
return scalar.parseLiteral(ast, variables);
|
|
1139
|
-
} catch (e) {
|
|
1140
|
-
logger3.error(e);
|
|
1141
|
-
throw exception;
|
|
821
|
+
},
|
|
822
|
+
parseLiteral(ast, variables) {
|
|
823
|
+
if (!errorMessage) {
|
|
824
|
+
return scalar.parseLiteral(ast, variables);
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
return scalar.parseLiteral(ast, variables);
|
|
828
|
+
} catch (e) {
|
|
829
|
+
logger.error(e);
|
|
830
|
+
throw exception;
|
|
1142
831
|
}
|
|
1143
832
|
}
|
|
1144
833
|
});
|
|
@@ -1328,8 +1017,7 @@ type Subscription {
|
|
|
1328
1017
|
_empty: String
|
|
1329
1018
|
}
|
|
1330
1019
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1020
|
+
${AUTHENTICATED_DIRECTIVE_SDL}
|
|
1333
1021
|
directive @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
|
|
1334
1022
|
|
|
1335
1023
|
directive @shareable on OBJECT | FIELD_DEFINITION
|
|
@@ -1339,11 +1027,429 @@ directive @link(url: String!, import: [String!]) on SCHEMA
|
|
|
1339
1027
|
`;
|
|
1340
1028
|
// src/upload.ts
|
|
1341
1029
|
import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs";
|
|
1030
|
+
// src/utils/change-resolvers.utils.ts
|
|
1031
|
+
import {
|
|
1032
|
+
AUDIT_EVENT,
|
|
1033
|
+
CommitType
|
|
1034
|
+
} from "@nxgt/shared-mongo";
|
|
1035
|
+
import { withFilter } from "graphql-subscriptions";
|
|
1036
|
+
function createChangeResolvers(config) {
|
|
1037
|
+
const { entity, collection, service, pubsub } = config;
|
|
1038
|
+
const resolvePubSub = () => typeof pubsub === "function" ? pubsub() : pubsub;
|
|
1039
|
+
return {
|
|
1040
|
+
Query: {
|
|
1041
|
+
[`${entity}Changes`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollection(collection, args),
|
|
1042
|
+
[`${entity}ChangesById`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollectionAndOid(args.id, collection, args)
|
|
1043
|
+
},
|
|
1044
|
+
Subscription: {
|
|
1045
|
+
[`${entity}Changed`]: {
|
|
1046
|
+
resolve: async (payload, _args, context) => ({
|
|
1047
|
+
id: context.services.audit.extractId(payload),
|
|
1048
|
+
data: payload.type !== CommitType.TERMINAL ? await context.services[service].findById(payload.state._id) : null,
|
|
1049
|
+
type: payload.type
|
|
1050
|
+
}),
|
|
1051
|
+
subscribe: withFilter(() => resolvePubSub().subscribe(AUDIT_EVENT), (payload, _variables, context) => context ? context.services.audit.matches(payload, { collection }) : false)
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
// src/utils/codegen.utils.ts
|
|
1057
|
+
var SCALARS_MAPPING = {
|
|
1058
|
+
ID: "any",
|
|
1059
|
+
DateTime: "any",
|
|
1060
|
+
Date: "any",
|
|
1061
|
+
Any: "any",
|
|
1062
|
+
BigInt: "number",
|
|
1063
|
+
EmailAddress: "string",
|
|
1064
|
+
GeoJSON: "any",
|
|
1065
|
+
HSL: "string",
|
|
1066
|
+
HSLA: "string",
|
|
1067
|
+
HexColorCode: "string",
|
|
1068
|
+
Hexadecimal: "string",
|
|
1069
|
+
IBAN: "string",
|
|
1070
|
+
IP: "string",
|
|
1071
|
+
Time: "string",
|
|
1072
|
+
LocalTime: "string",
|
|
1073
|
+
UUID: "string",
|
|
1074
|
+
PositiveInt: "number",
|
|
1075
|
+
NegativeInt: "number",
|
|
1076
|
+
NonPositiveInt: "number",
|
|
1077
|
+
NonNegativeInt: "number",
|
|
1078
|
+
PositiveFloat: "number",
|
|
1079
|
+
NegativeFloat: "number",
|
|
1080
|
+
NonPositiveFloat: "number",
|
|
1081
|
+
NonNegativeFloat: "number",
|
|
1082
|
+
NonEmptyString: "string",
|
|
1083
|
+
PostalCode: "string",
|
|
1084
|
+
RGB: "string",
|
|
1085
|
+
RGBA: "string",
|
|
1086
|
+
SESSN: "string",
|
|
1087
|
+
Timestamp: "string",
|
|
1088
|
+
URL: "string",
|
|
1089
|
+
USCurrency: "string",
|
|
1090
|
+
UTCOffset: "string",
|
|
1091
|
+
Long: "number",
|
|
1092
|
+
Short: "number",
|
|
1093
|
+
Byte: "number",
|
|
1094
|
+
BigDecimal: "string",
|
|
1095
|
+
BigInteger: "string",
|
|
1096
|
+
Char: "string",
|
|
1097
|
+
Locale: "string",
|
|
1098
|
+
CountryCode: "string",
|
|
1099
|
+
CountryName: "string",
|
|
1100
|
+
AccountNumber: "string",
|
|
1101
|
+
CurrencyCode: "string",
|
|
1102
|
+
Duration: "string",
|
|
1103
|
+
Url: "string",
|
|
1104
|
+
JSONObject: "Record<string, any>",
|
|
1105
|
+
JSON: "any",
|
|
1106
|
+
Void: "void",
|
|
1107
|
+
ObjectID: "any",
|
|
1108
|
+
Upload: "File | Blob",
|
|
1109
|
+
Email: "string",
|
|
1110
|
+
Port: "number",
|
|
1111
|
+
IPv4: "string",
|
|
1112
|
+
IPv6: "string",
|
|
1113
|
+
MAC: "string",
|
|
1114
|
+
Password: "string",
|
|
1115
|
+
PhoneNumber: "string",
|
|
1116
|
+
ZIPCode: "string",
|
|
1117
|
+
SSN: "string",
|
|
1118
|
+
GUID: "string",
|
|
1119
|
+
ISBN: "string",
|
|
1120
|
+
Latitude: "number",
|
|
1121
|
+
Longitude: "number",
|
|
1122
|
+
JWT: "string"
|
|
1123
|
+
};
|
|
1124
|
+
// src/utils/data-loader.utils.ts
|
|
1125
|
+
import { cast } from "@nxgt/shared/helpers";
|
|
1126
|
+
import {
|
|
1127
|
+
paginateList
|
|
1128
|
+
} from "@nxgt/shared-mongo";
|
|
1129
|
+
import DataLoader2 from "dataloader";
|
|
1130
|
+
import { isArray } from "lodash";
|
|
1131
|
+
function createDataLoader(batchLoadFn, options) {
|
|
1132
|
+
return new DataLoader2(batchLoadFn, options);
|
|
1133
|
+
}
|
|
1134
|
+
function createConnectionListDataLoader(path, model, options) {
|
|
1135
|
+
return createDataLoader(async (docs) => {
|
|
1136
|
+
const data = await model.find({
|
|
1137
|
+
...options.filter ?? {},
|
|
1138
|
+
_id: docs.flatMap((doc) => {
|
|
1139
|
+
const ids = doc[path] ?? [];
|
|
1140
|
+
return isArray(ids) ? cast(ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))) : [];
|
|
1141
|
+
})
|
|
1142
|
+
}).exec();
|
|
1143
|
+
return cast(docs.map((doc) => model.paginateList(data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString())), options)));
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
function createChildrenConnectionDataLoader(path, model, options) {
|
|
1147
|
+
return createDataLoader(async (docs) => {
|
|
1148
|
+
const filter = {};
|
|
1149
|
+
filter[path] = docs.map((doc) => doc._id);
|
|
1150
|
+
const data = await model.find(filter).exec();
|
|
1151
|
+
return cast(docs.map((doc) => paginateList(data.filter((child) => doc._id?.toString() === child[path]?.toString()), options)));
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
function createSimpleListDataLoader(path, model, options) {
|
|
1155
|
+
return createDataLoader(async (docs) => {
|
|
1156
|
+
const filter = options?.filter ?? {};
|
|
1157
|
+
const ids = docs.flatMap((doc) => {
|
|
1158
|
+
const ids = doc[path] ?? [];
|
|
1159
|
+
return isArray(ids) ? cast(ids) : [];
|
|
1160
|
+
});
|
|
1161
|
+
filter._id = ids;
|
|
1162
|
+
const query = model.find(filter);
|
|
1163
|
+
const data = await query.exec();
|
|
1164
|
+
return cast(docs.map((doc) => {
|
|
1165
|
+
const result = data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString()));
|
|
1166
|
+
if (options?.skip && options?.limit) {
|
|
1167
|
+
return result.slice(options.skip, options.skip + options.limit);
|
|
1168
|
+
}
|
|
1169
|
+
if (options?.limit) {
|
|
1170
|
+
return result.slice(0, options.limit);
|
|
1171
|
+
}
|
|
1172
|
+
if (options?.skip) {
|
|
1173
|
+
return result.slice(options.skip);
|
|
1174
|
+
}
|
|
1175
|
+
return result;
|
|
1176
|
+
}));
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
function createChildrenListDataLoader(path, model, options) {
|
|
1180
|
+
return createDataLoader(async (docs) => {
|
|
1181
|
+
const filter = options?.filter ?? {};
|
|
1182
|
+
const key = path;
|
|
1183
|
+
filter[key] = docs.map((doc) => doc._id);
|
|
1184
|
+
const query = model.find(filter);
|
|
1185
|
+
const data = await query.exec();
|
|
1186
|
+
return cast(docs.map((doc) => {
|
|
1187
|
+
const result = data.filter((child) => child[path]?.toString() === doc._id.toString());
|
|
1188
|
+
if (options?.skip && options?.limit) {
|
|
1189
|
+
return result.slice(options.skip, options.skip + options.limit);
|
|
1190
|
+
}
|
|
1191
|
+
if (options?.limit) {
|
|
1192
|
+
return result.slice(0, options.limit);
|
|
1193
|
+
}
|
|
1194
|
+
if (options?.skip) {
|
|
1195
|
+
return result.slice(options.skip);
|
|
1196
|
+
}
|
|
1197
|
+
return result;
|
|
1198
|
+
}));
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
function createSingleChildDataLoader(path, model) {
|
|
1202
|
+
return createDataLoader(async (docs) => {
|
|
1203
|
+
const data = await model.find({
|
|
1204
|
+
_id: docs.map((doc) => cast(doc[path])).filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
|
|
1205
|
+
}).exec();
|
|
1206
|
+
const map = new Map(data.map((doc) => [doc.id, doc]));
|
|
1207
|
+
return docs.map((doc) => map.get(doc[path]?.toString() ?? "") ?? null);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
function createEntitiesReferenceLoader(model) {
|
|
1211
|
+
return createDataLoader(async (ids) => {
|
|
1212
|
+
const docs = await model.find({
|
|
1213
|
+
_id: ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
|
|
1214
|
+
}).exec();
|
|
1215
|
+
const map = new Map(docs.map((doc) => [doc.id, doc]));
|
|
1216
|
+
return ids.map((id) => map.get(id) ?? null);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
// src/utils/errors/create-error.ts
|
|
1220
|
+
import { GraphQLError as GraphQLError3 } from "graphql";
|
|
1221
|
+
function createGraphQLError(message, options) {
|
|
1222
|
+
return new GraphQLError3(message, options);
|
|
1223
|
+
}
|
|
1224
|
+
// src/utils/errors/format-error.ts
|
|
1225
|
+
import {
|
|
1226
|
+
ApolloServerErrorCode,
|
|
1227
|
+
unwrapResolverError
|
|
1228
|
+
} from "@apollo/server/errors";
|
|
1229
|
+
import {
|
|
1230
|
+
translate as translateBase
|
|
1231
|
+
} from "@nxgt/i18n";
|
|
1232
|
+
import { CustomException as CustomException2, ErrorCode as ErrorCode6 } from "@nxgt/shared-exceptions";
|
|
1233
|
+
import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
|
|
1234
|
+
import { kebabCase } from "lodash";
|
|
1235
|
+
function createFormatError(translate = translateBase, production) {
|
|
1236
|
+
return (formattedError, graphQLError) => {
|
|
1237
|
+
if (production && formattedError.extensions?.stacktrace) {
|
|
1238
|
+
formattedError.extensions.stacktrace = undefined;
|
|
1239
|
+
}
|
|
1240
|
+
const error = unwrapResolverError(graphQLError);
|
|
1241
|
+
if (isOryUnavailable(error)) {
|
|
1242
|
+
const unavailable = serviceUnavailableError(error);
|
|
1243
|
+
return {
|
|
1244
|
+
...formattedError,
|
|
1245
|
+
message: unavailable.message,
|
|
1246
|
+
extensions: { ...formattedError.extensions, ...unavailable.extensions }
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
const key = denialMessageKey(error);
|
|
1250
|
+
if (key !== undefined) {
|
|
1251
|
+
return { ...formattedError, message: translate(key) };
|
|
1252
|
+
}
|
|
1253
|
+
if (error instanceof CustomException2 || error instanceof mongoose.MongooseError) {
|
|
1254
|
+
const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
|
|
1255
|
+
return {
|
|
1256
|
+
...formattedError,
|
|
1257
|
+
message: exception.errorCode === ErrorCode6.ValidationError ? exception.message : translate(exception.message, exception.options),
|
|
1258
|
+
extensions: {
|
|
1259
|
+
...formattedError.extensions,
|
|
1260
|
+
code: exception.errorCode,
|
|
1261
|
+
debugMessage: exception.debugMessage
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
if ([
|
|
1266
|
+
ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED,
|
|
1267
|
+
ApolloServerErrorCode.BAD_REQUEST,
|
|
1268
|
+
ApolloServerErrorCode.BAD_USER_INPUT,
|
|
1269
|
+
ApolloServerErrorCode.GRAPHQL_PARSE_FAILED,
|
|
1270
|
+
ApolloServerErrorCode.OPERATION_RESOLUTION_FAILURE,
|
|
1271
|
+
ApolloServerErrorCode.PERSISTED_QUERY_NOT_FOUND,
|
|
1272
|
+
ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED
|
|
1273
|
+
].includes(formattedError.extensions?.code)) {
|
|
1274
|
+
return {
|
|
1275
|
+
...formattedError,
|
|
1276
|
+
message: translate(`errors.${kebabCase(formattedError.extensions?.code)}`, {}),
|
|
1277
|
+
extensions: {
|
|
1278
|
+
...formattedError.extensions,
|
|
1279
|
+
debugMessage: formattedError.message
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
return formattedError;
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
// src/utils/errors/mask-error.ts
|
|
1287
|
+
import {
|
|
1288
|
+
translate as translateBase2
|
|
1289
|
+
} from "@nxgt/i18n";
|
|
1290
|
+
import { CustomException as CustomException3, ErrorCode as ErrorCode7 } from "@nxgt/shared-exceptions";
|
|
1291
|
+
import { MONGO_UTILS as MONGO_UTILS2, mongoose as mongoose2 } from "@nxgt/shared-mongo";
|
|
1292
|
+
import { GraphQLError as GraphQLError4 } from "graphql";
|
|
1293
|
+
function createMaskError(translate = translateBase2) {
|
|
1294
|
+
return (error, message, isDev) => {
|
|
1295
|
+
const original = error instanceof GraphQLError4 && error.originalError ? error.originalError : error;
|
|
1296
|
+
if (isOryUnavailable(original))
|
|
1297
|
+
return serviceUnavailableError(original);
|
|
1298
|
+
const key = denialMessageKey(original);
|
|
1299
|
+
if (key !== undefined) {
|
|
1300
|
+
return new GraphQLError4(translate(key), {
|
|
1301
|
+
...locationOf(error),
|
|
1302
|
+
extensions: original.extensions
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
if (original instanceof CustomException3 || original instanceof mongoose2.MongooseError) {
|
|
1306
|
+
return exceptionError(original, translate);
|
|
1307
|
+
}
|
|
1308
|
+
if (isOriginalGraphQLError(error))
|
|
1309
|
+
return error;
|
|
1310
|
+
return new GraphQLError4(message, {
|
|
1311
|
+
...locationOf(error),
|
|
1312
|
+
extensions: isDev ? {
|
|
1313
|
+
code: ErrorCode7.InternalServerError,
|
|
1314
|
+
debugMessage: String(original)
|
|
1315
|
+
} : { code: ErrorCode7.InternalServerError }
|
|
1316
|
+
});
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
function exceptionError(original, translate) {
|
|
1320
|
+
const exception = original instanceof mongoose2.MongooseError ? MONGO_UTILS2.castError(original) : original;
|
|
1321
|
+
const status = exception.code;
|
|
1322
|
+
return new GraphQLError4(exception.errorCode === ErrorCode7.ValidationError ? exception.message : translate(exception.message, exception.options), {
|
|
1323
|
+
extensions: {
|
|
1324
|
+
code: exception.errorCode,
|
|
1325
|
+
...status ? { http: { status } } : {},
|
|
1326
|
+
...exception.debugMessage ? { debugMessage: exception.debugMessage } : {}
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
function isOriginalGraphQLError(error) {
|
|
1331
|
+
if (!(error instanceof GraphQLError4))
|
|
1332
|
+
return false;
|
|
1333
|
+
return error.originalError == null ? true : isOriginalGraphQLError(error.originalError);
|
|
1334
|
+
}
|
|
1335
|
+
function locationOf(error) {
|
|
1336
|
+
if (!(error instanceof GraphQLError4))
|
|
1337
|
+
return {};
|
|
1338
|
+
return {
|
|
1339
|
+
nodes: error.nodes,
|
|
1340
|
+
source: error.source,
|
|
1341
|
+
positions: error.positions,
|
|
1342
|
+
path: error.path
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
// src/utils/publish.ts
|
|
1346
|
+
import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
|
|
1347
|
+
import { createPubSub } from "graphql-yoga";
|
|
1348
|
+
var createYogaPubSub = ({
|
|
1349
|
+
publishClient,
|
|
1350
|
+
subscribeClient
|
|
1351
|
+
}) => {
|
|
1352
|
+
const eventTarget = createRedisEventTarget({
|
|
1353
|
+
publishClient,
|
|
1354
|
+
subscribeClient
|
|
1355
|
+
});
|
|
1356
|
+
return createPubSub({ eventTarget });
|
|
1357
|
+
};
|
|
1358
|
+
// src/utils/schema.utils.ts
|
|
1359
|
+
import { existsSync } from "node:fs";
|
|
1360
|
+
import { dirname, join } from "node:path";
|
|
1361
|
+
import { fileURLToPath } from "node:url";
|
|
1362
|
+
import { buildSubgraphSchema as buildSubgraphSchemaBase } from "@apollo/subgraph";
|
|
1363
|
+
import { loadFilesSync } from "@graphql-tools/load-files";
|
|
1364
|
+
import { mergeTypeDefs } from "@graphql-tools/merge";
|
|
1365
|
+
import { pruneSchema } from "@graphql-tools/utils";
|
|
1366
|
+
import { printSchema } from "graphql";
|
|
1367
|
+
function packageRoot() {
|
|
1368
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
1369
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
1370
|
+
const parent = dirname(dir);
|
|
1371
|
+
if (parent === dir)
|
|
1372
|
+
throw new Error("@nxgt/shared-graphql: no package root");
|
|
1373
|
+
dir = parent;
|
|
1374
|
+
}
|
|
1375
|
+
return dir;
|
|
1376
|
+
}
|
|
1377
|
+
var SHARED_SCHEMA_PATH = join(packageRoot(), "graphql/**/*.graphqls");
|
|
1378
|
+
function loadTypeDefs(...paths) {
|
|
1379
|
+
const typeDefs = mergeTypeDefs([
|
|
1380
|
+
...paths.flatMap((path) => loadFilesSync(path, {
|
|
1381
|
+
recursive: true
|
|
1382
|
+
}))
|
|
1383
|
+
]);
|
|
1384
|
+
return typeDefs;
|
|
1385
|
+
}
|
|
1386
|
+
function buildSubgraphSchema(modulesOrSdl) {
|
|
1387
|
+
return pruneSchema(buildSubgraphSchemaBase(modulesOrSdl), {
|
|
1388
|
+
skipUnusedTypesPruning: false
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
async function generateSchema(output, ...paths) {
|
|
1392
|
+
await Bun.write(output, printSchema(buildSubgraphSchema(loadTypeDefs(...paths))));
|
|
1393
|
+
}
|
|
1394
|
+
// src/utils/websocket.ts
|
|
1395
|
+
import { createServer } from "node:http";
|
|
1396
|
+
import { useServer } from "graphql-ws/use/ws";
|
|
1397
|
+
import { WebSocketServer } from "ws";
|
|
1398
|
+
function setupYogaWebSocketServer({
|
|
1399
|
+
yoga,
|
|
1400
|
+
context
|
|
1401
|
+
}) {
|
|
1402
|
+
const httpServer = createServer(yoga);
|
|
1403
|
+
const wsServer = new WebSocketServer({
|
|
1404
|
+
server: httpServer,
|
|
1405
|
+
path: yoga.graphqlEndpoint
|
|
1406
|
+
});
|
|
1407
|
+
useServer({
|
|
1408
|
+
execute: (args) => args.rootValue.execute(args),
|
|
1409
|
+
subscribe: (args) => args.rootValue.subscribe(args),
|
|
1410
|
+
onSubscribe: async (ctx, _id, params) => {
|
|
1411
|
+
const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped({
|
|
1412
|
+
...ctx,
|
|
1413
|
+
req: ctx.extra.request,
|
|
1414
|
+
socket: ctx.extra.socket,
|
|
1415
|
+
params
|
|
1416
|
+
});
|
|
1417
|
+
const args = {
|
|
1418
|
+
schema,
|
|
1419
|
+
operationName: params.operationName,
|
|
1420
|
+
document: parse(params.query),
|
|
1421
|
+
variableValues: params.variables,
|
|
1422
|
+
contextValue: await contextFactory(),
|
|
1423
|
+
rootValue: {
|
|
1424
|
+
execute,
|
|
1425
|
+
subscribe
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
const errors = validate(args.schema, args.document);
|
|
1429
|
+
if (errors.length)
|
|
1430
|
+
return errors;
|
|
1431
|
+
return args;
|
|
1432
|
+
},
|
|
1433
|
+
context
|
|
1434
|
+
}, wsServer);
|
|
1435
|
+
const startServer = (port) => new Promise((resolve) => httpServer.listen({ port }, resolve));
|
|
1436
|
+
return { yoga, httpServer, startServer };
|
|
1437
|
+
}
|
|
1438
|
+
// src/utils/ws-context.ts
|
|
1439
|
+
async function resolveWsUser(connectionParams, authClient) {
|
|
1440
|
+
const rawToken = connectionParams?.authorization ?? connectionParams?.Authorization;
|
|
1441
|
+
const token = rawToken?.replace("Bearer ", "");
|
|
1442
|
+
const { data } = token ? await authClient.POST("/oauth/introspect", { body: { token } }) : { data: null };
|
|
1443
|
+
const user = data?.active ? { ...data, name: data.sub } : undefined;
|
|
1444
|
+
return { user, token };
|
|
1445
|
+
}
|
|
1342
1446
|
export {
|
|
1343
|
-
|
|
1344
|
-
|
|
1447
|
+
AUTHENTICATED_DIRECTIVE_NAME,
|
|
1448
|
+
AUTHENTICATED_DIRECTIVE_SDL,
|
|
1449
|
+
CALLER_TYPES,
|
|
1345
1450
|
DataLoader3 as DataLoader,
|
|
1346
1451
|
FEDERATION_DIRECTIVES,
|
|
1452
|
+
GATEWAY_SECRET_HEADER,
|
|
1347
1453
|
KETO_DIRECTIVES_SDL,
|
|
1348
1454
|
PERMISSION_DIRECTIVE_NAME,
|
|
1349
1455
|
PERMISSION_DIRECTIVE_SDL,
|
|
@@ -1351,6 +1457,7 @@ export {
|
|
|
1351
1457
|
SCALAR_RESOLVERS,
|
|
1352
1458
|
SHARED_SCHEMA_PATH,
|
|
1353
1459
|
SHARED_TYPE_DEFS,
|
|
1460
|
+
applyAuthenticated,
|
|
1354
1461
|
applyKetoChecks,
|
|
1355
1462
|
assertReadablePath,
|
|
1356
1463
|
buildSubgraphSchema,
|
|
@@ -1369,7 +1476,10 @@ export {
|
|
|
1369
1476
|
createSingleChildDataLoader,
|
|
1370
1477
|
createYogaHono,
|
|
1371
1478
|
createYogaPubSub,
|
|
1479
|
+
denial,
|
|
1480
|
+
denialMessageKey,
|
|
1372
1481
|
extractJwtPlugin,
|
|
1482
|
+
gatewaySecret,
|
|
1373
1483
|
generateSchema,
|
|
1374
1484
|
graphqlUploadExpress,
|
|
1375
1485
|
honoYoga,
|
|
@@ -1377,22 +1487,23 @@ export {
|
|
|
1377
1487
|
loadTypeDefs,
|
|
1378
1488
|
objectIds,
|
|
1379
1489
|
oryUnavailableError,
|
|
1380
|
-
|
|
1490
|
+
principalOf,
|
|
1381
1491
|
readPath,
|
|
1382
1492
|
readPermissions,
|
|
1383
|
-
readRequirements,
|
|
1384
1493
|
renderSandbox,
|
|
1494
|
+
requireGatewayTrust,
|
|
1385
1495
|
requireUser,
|
|
1386
1496
|
resolveOryPrincipal,
|
|
1387
1497
|
resolveWsUser,
|
|
1388
|
-
|
|
1498
|
+
sandboxExplorer,
|
|
1389
1499
|
serviceUnavailableError,
|
|
1390
1500
|
setupYogaWebSocketServer,
|
|
1391
1501
|
toPrincipal,
|
|
1392
1502
|
useAuth,
|
|
1503
|
+
useAuthenticated,
|
|
1393
1504
|
useKetoChecks,
|
|
1394
1505
|
useOryAuth
|
|
1395
1506
|
};
|
|
1396
1507
|
|
|
1397
|
-
//# debugId=
|
|
1508
|
+
//# debugId=A1D566582D44C98864756E2164756E21
|
|
1398
1509
|
//# sourceMappingURL=index.js.map
|