@nxgt/shared-graphql 2.1.0 → 3.0.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 +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 +1055 -927
- 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 +47 -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,442 @@ 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
|
-
return docs.map((doc) => map.get(doc[path]?.toString() ?? "") ?? null);
|
|
510
|
-
});
|
|
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
511
|
}
|
|
512
|
-
function
|
|
513
|
-
return
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
512
|
+
function refusal(onDeny, message) {
|
|
513
|
+
return denial(onDeny === "FORBIDDEN" ? ErrorCode3.Forbidden : ErrorCode3.NotFound, message);
|
|
514
|
+
}
|
|
515
|
+
// src/plugins/gateway-trust.ts
|
|
516
|
+
var GATEWAY_SECRET_HEADER = "x-gateway-secret";
|
|
517
|
+
var MIN_SECRET_LENGTH = 16;
|
|
518
|
+
function gatewaySecret({
|
|
519
|
+
secret,
|
|
520
|
+
header = GATEWAY_SECRET_HEADER
|
|
521
|
+
}) {
|
|
522
|
+
if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
|
|
523
|
+
throw new TypeError(`gatewaySecret(): \`secret\` must be a string of at least ${MIN_SECRET_LENGTH} characters — is its environment variable set?`);
|
|
524
|
+
}
|
|
525
|
+
const expected = new TextEncoder().encode(secret);
|
|
526
|
+
return (headers) => {
|
|
527
|
+
const sent = headers.get(header);
|
|
528
|
+
if (typeof sent !== "string")
|
|
529
|
+
return false;
|
|
530
|
+
return constantTimeEqual(new TextEncoder().encode(sent), expected);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function requireGatewayTrust(options, plugin) {
|
|
534
|
+
const trust = options?.trustedGateway;
|
|
535
|
+
if (typeof trust !== "function") {
|
|
536
|
+
throw new TypeError(`${plugin}: name the gateway allowed to set the caller — ${plugin.replace("()", "")}({ trustedGateway: gatewaySecret({ secret }) }). The request's extensions are written by the client; an API that resolves its own callers uses useOryAuth(ory)`);
|
|
537
|
+
}
|
|
538
|
+
return trust;
|
|
539
|
+
}
|
|
540
|
+
function principalOf(value) {
|
|
541
|
+
if (!value || typeof value !== "object")
|
|
542
|
+
return;
|
|
543
|
+
const sub = value.sub;
|
|
544
|
+
return typeof sub === "string" && sub.length > 0 ? value : undefined;
|
|
545
|
+
}
|
|
546
|
+
function constantTimeEqual(sent, expected) {
|
|
547
|
+
let difference = sent.length ^ expected.length;
|
|
548
|
+
for (let index = 0;index < expected.length; index++) {
|
|
549
|
+
difference |= (sent[index] ?? 0) ^ (expected[index] ?? 0);
|
|
550
|
+
}
|
|
551
|
+
return difference === 0;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/plugins/auth.ts
|
|
555
|
+
function useAuth(options) {
|
|
556
|
+
const trusted = requireGatewayTrust(options, "useAuth()");
|
|
557
|
+
return {
|
|
558
|
+
onContextBuilding: async ({ context, extendContext }) => {
|
|
559
|
+
const headers = context.request?.headers;
|
|
560
|
+
if (typeof headers?.get !== "function")
|
|
561
|
+
return;
|
|
562
|
+
if (!await trusted(headers))
|
|
563
|
+
return;
|
|
564
|
+
const user = principalOf(context.params?.extensions?.user);
|
|
565
|
+
if (!user)
|
|
566
|
+
return;
|
|
567
|
+
const token = context.params.extensions?.token;
|
|
568
|
+
extendContext({
|
|
569
|
+
user,
|
|
570
|
+
token: typeof token === "string" ? token : undefined
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// src/plugins/authenticated.ts
|
|
576
|
+
function useAuthenticated(options = {}) {
|
|
577
|
+
return {
|
|
578
|
+
onSchemaChange: ({ schema, replaceSchema }) => {
|
|
579
|
+
const next = applyAuthenticated(schema, options);
|
|
580
|
+
if (next !== schema)
|
|
581
|
+
replaceSchema(next);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
// src/plugins/extract-jwt.ts
|
|
586
|
+
function extractJwtPlugin(options) {
|
|
587
|
+
const trusted = requireGatewayTrust(options, "extractJwtPlugin()");
|
|
588
|
+
return {
|
|
589
|
+
async requestDidStart({ request, contextValue }) {
|
|
590
|
+
const headers = request.http?.headers;
|
|
591
|
+
if (!headers || !await trusted(headers))
|
|
592
|
+
return;
|
|
593
|
+
const payload = principalOf(request.extensions?.payload);
|
|
594
|
+
if (payload)
|
|
595
|
+
contextValue.jwt = { payload };
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// src/plugins/keto-checker.ts
|
|
600
|
+
import { tuple } from "@nxgt/ory-sdk";
|
|
601
|
+
import DataLoader from "dataloader";
|
|
602
|
+
function createKetoChecks(ory) {
|
|
603
|
+
const loader = new DataLoader((questions) => ory.checkMany([...questions]), {
|
|
604
|
+
cacheKeyFn: ({ permission, subject }) => tuple(permission, subject)
|
|
519
605
|
});
|
|
606
|
+
return (permission, subject) => loader.load({ permission, subject });
|
|
520
607
|
}
|
|
521
|
-
// src/
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
608
|
+
// src/plugins/keto-checks.ts
|
|
609
|
+
function useKetoChecks(ory, options = {}) {
|
|
610
|
+
return {
|
|
611
|
+
onSchemaChange: ({ schema, replaceSchema }) => {
|
|
612
|
+
const next = applyKetoChecks(schema, options);
|
|
613
|
+
if (next !== schema)
|
|
614
|
+
replaceSchema(next);
|
|
615
|
+
},
|
|
616
|
+
onContextBuilding: ({ extendContext }) => {
|
|
617
|
+
extendContext({ ketoChecks: createKetoChecks(ory) });
|
|
618
|
+
}
|
|
619
|
+
};
|
|
525
620
|
}
|
|
526
|
-
// src/
|
|
527
|
-
import {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
621
|
+
// src/plugins/keto-helpers.ts
|
|
622
|
+
import { ErrorCode as ErrorCode4 } from "@nxgt/shared-exceptions";
|
|
623
|
+
function requireUser(context) {
|
|
624
|
+
if (!context.user) {
|
|
625
|
+
throw denial(ErrorCode4.Unauthenticated);
|
|
626
|
+
}
|
|
627
|
+
return context.user;
|
|
628
|
+
}
|
|
629
|
+
async function can(context, question) {
|
|
630
|
+
const subject = context.ory?.subject;
|
|
631
|
+
if (!subject) {
|
|
632
|
+
throw denial(ErrorCode4.Unauthenticated);
|
|
633
|
+
}
|
|
634
|
+
if (!context.ketoChecks) {
|
|
635
|
+
throw new Error("can(): no checker on the context — useKetoChecks(ory) is not registered");
|
|
636
|
+
}
|
|
637
|
+
const permission = {
|
|
638
|
+
namespace: question.type,
|
|
639
|
+
object: question.id,
|
|
640
|
+
relation: question.name
|
|
641
|
+
};
|
|
642
|
+
return context.ketoChecks(permission, subject);
|
|
643
|
+
}
|
|
644
|
+
// src/plugins/ory-auth.ts
|
|
531
645
|
import {
|
|
532
|
-
|
|
533
|
-
} from "@nxgt/
|
|
534
|
-
import {
|
|
535
|
-
import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
|
|
536
|
-
import { kebabCase } from "lodash";
|
|
646
|
+
bearerOf
|
|
647
|
+
} from "@nxgt/ory-sdk";
|
|
648
|
+
import { claimsFromOryPrincipal } from "@nxgt/security/integrations/ory";
|
|
537
649
|
|
|
538
650
|
// src/utils/errors/ory-unavailable.ts
|
|
539
|
-
import { ErrorCode } from "@nxgt/shared-exceptions";
|
|
651
|
+
import { ErrorCode as ErrorCode5 } from "@nxgt/shared-exceptions";
|
|
540
652
|
import { GraphQLError as GraphQLError2 } from "graphql";
|
|
541
653
|
function isOryUnavailable(error) {
|
|
542
654
|
if (!(error instanceof Error))
|
|
@@ -547,598 +659,192 @@ function isOryUnavailable(error) {
|
|
|
547
659
|
function serviceUnavailableError(error) {
|
|
548
660
|
return new GraphQLError2(`ory: ${error.service} is unavailable`, {
|
|
549
661
|
extensions: {
|
|
550
|
-
code:
|
|
662
|
+
code: ErrorCode5.ServiceUnavailable,
|
|
551
663
|
http: { status: 503 },
|
|
552
664
|
debugMessage: error.message
|
|
553
665
|
}
|
|
554
666
|
});
|
|
555
667
|
}
|
|
556
668
|
|
|
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
|
-
});
|
|
669
|
+
// src/plugins/ory-auth.ts
|
|
670
|
+
function toPrincipal(ory) {
|
|
671
|
+
const email = ory.identity?.email;
|
|
672
|
+
return {
|
|
673
|
+
sub: ory.subject,
|
|
674
|
+
uid: ory.subject,
|
|
675
|
+
name: email ?? ory.clientId ?? ory.subject,
|
|
676
|
+
username: email,
|
|
677
|
+
clientId: ory.clientId,
|
|
678
|
+
scope: ory.scopes.join(" ") || undefined,
|
|
679
|
+
tokenType: ory.kind,
|
|
680
|
+
exp: ory.expiresAt ? Math.floor(ory.expiresAt.getTime() / 1000) : undefined,
|
|
681
|
+
authorities: [],
|
|
682
|
+
roles: []
|
|
629
683
|
};
|
|
630
684
|
}
|
|
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
|
-
});
|
|
685
|
+
function oryUnavailableError(error) {
|
|
686
|
+
return serviceUnavailableError(error);
|
|
641
687
|
}
|
|
642
|
-
function
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
688
|
+
async function resolveOryPrincipal(ory, headers) {
|
|
689
|
+
let principal;
|
|
690
|
+
try {
|
|
691
|
+
principal = await ory.resolve(headers);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
if (isOryUnavailable(error))
|
|
694
|
+
throw oryUnavailableError(error);
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
ory: principal,
|
|
699
|
+
user: principal ? toPrincipal(principal) : undefined,
|
|
700
|
+
claims: principal ? claimsFromOryPrincipal(principal) : undefined,
|
|
701
|
+
token: bearerOf(headers) ?? undefined
|
|
702
|
+
};
|
|
646
703
|
}
|
|
647
|
-
function
|
|
648
|
-
if (!(error instanceof GraphQLError3))
|
|
649
|
-
return {};
|
|
704
|
+
function useOryAuth(ory) {
|
|
650
705
|
return {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
path: error.path
|
|
706
|
+
onContextBuilding: async ({ context, extendContext }) => {
|
|
707
|
+
extendContext(await resolveOryPrincipal(ory, context.request.headers));
|
|
708
|
+
}
|
|
655
709
|
};
|
|
656
710
|
}
|
|
657
|
-
// src/
|
|
658
|
-
import {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
711
|
+
// src/scalars/any/any.scalars.ts
|
|
712
|
+
import { GraphQLScalarType, Kind } from "graphql";
|
|
713
|
+
var ANY_SCALAR = {
|
|
714
|
+
Any: new GraphQLScalarType({
|
|
715
|
+
name: "Any",
|
|
716
|
+
description: "Generic scalar to represent any type",
|
|
717
|
+
serialize(value) {
|
|
718
|
+
return value;
|
|
719
|
+
},
|
|
720
|
+
parseValue(value) {
|
|
721
|
+
return value;
|
|
722
|
+
},
|
|
723
|
+
parseLiteral(ast, variables) {
|
|
724
|
+
switch (ast.kind) {
|
|
725
|
+
case Kind.STRING:
|
|
726
|
+
case Kind.BOOLEAN:
|
|
727
|
+
return ast.value;
|
|
728
|
+
case Kind.INT:
|
|
729
|
+
case Kind.FLOAT:
|
|
730
|
+
return parseFloat(ast.value);
|
|
731
|
+
case Kind.OBJECT:
|
|
732
|
+
return Object.fromEntries(ast.fields.map((field) => [field.name, field.value]));
|
|
733
|
+
case Kind.LIST:
|
|
734
|
+
return ast.values.map((n) => this?.parseLiteral?.(n, variables));
|
|
735
|
+
case Kind.NULL:
|
|
736
|
+
return null;
|
|
737
|
+
case Kind.VARIABLE: {
|
|
738
|
+
const name = ast.name.value;
|
|
739
|
+
return variables ? variables[name] : undefined;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
})
|
|
669
744
|
};
|
|
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
|
-
|
|
745
|
+
// src/scalars/custom/custom.scalars.ts
|
|
746
|
+
import {
|
|
747
|
+
AccountNumberResolver,
|
|
748
|
+
BigIntResolver,
|
|
749
|
+
CountryCodeResolver,
|
|
750
|
+
CountryNameResolver,
|
|
751
|
+
CuidResolver,
|
|
752
|
+
CurrencyResolver,
|
|
753
|
+
DateResolver,
|
|
754
|
+
DateTimeISOResolver,
|
|
755
|
+
DurationResolver,
|
|
756
|
+
EmailAddressResolver,
|
|
757
|
+
GeoJSONResolver,
|
|
758
|
+
GUIDResolver,
|
|
759
|
+
HexadecimalResolver,
|
|
760
|
+
HexColorCodeResolver,
|
|
761
|
+
HSLAResolver,
|
|
762
|
+
HSLResolver,
|
|
763
|
+
IBANResolver,
|
|
764
|
+
IPResolver,
|
|
765
|
+
IPv4Resolver,
|
|
766
|
+
IPv6Resolver,
|
|
767
|
+
ISBNResolver,
|
|
768
|
+
JSONObjectResolver,
|
|
769
|
+
JSONResolver,
|
|
770
|
+
JWTResolver,
|
|
771
|
+
LatitudeResolver,
|
|
772
|
+
LocaleResolver,
|
|
773
|
+
LongitudeResolver,
|
|
774
|
+
LongResolver,
|
|
775
|
+
MACResolver,
|
|
776
|
+
NegativeFloatResolver,
|
|
777
|
+
NegativeIntResolver,
|
|
778
|
+
NonEmptyStringResolver,
|
|
779
|
+
NonNegativeFloatResolver,
|
|
780
|
+
NonNegativeIntResolver,
|
|
781
|
+
NonPositiveFloatResolver,
|
|
782
|
+
NonPositiveIntResolver,
|
|
783
|
+
ObjectIDResolver,
|
|
784
|
+
PhoneNumberResolver,
|
|
785
|
+
PortResolver,
|
|
786
|
+
PositiveFloatResolver,
|
|
787
|
+
PositiveIntResolver,
|
|
788
|
+
PostalCodeResolver,
|
|
789
|
+
RGBAResolver,
|
|
790
|
+
RGBResolver,
|
|
791
|
+
SESSNResolver,
|
|
792
|
+
TimeResolver,
|
|
793
|
+
TimestampResolver,
|
|
794
|
+
URLResolver,
|
|
795
|
+
USCurrencyResolver,
|
|
796
|
+
UtcOffsetResolver,
|
|
797
|
+
UUIDResolver,
|
|
798
|
+
VoidResolver
|
|
799
|
+
} from "graphql-scalars";
|
|
700
800
|
|
|
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
|
|
801
|
+
// src/scalars/custom/utils.ts
|
|
802
|
+
import { CustomException } from "@nxgt/shared-exceptions";
|
|
803
|
+
import { logger } from "@nxgt/shared-logging";
|
|
804
|
+
import { GraphQLScalarType as GraphQLScalarType2 } from "graphql";
|
|
805
|
+
function createScalarFrom(scalar, {
|
|
806
|
+
name,
|
|
807
|
+
description,
|
|
808
|
+
errorMessage
|
|
745
809
|
}) {
|
|
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);
|
|
810
|
+
const exception = CustomException.badRequest({
|
|
811
|
+
message: errorMessage
|
|
813
812
|
});
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
refuseOnInterface(schema, fieldConfig, `${typeName}.${fieldName}`);
|
|
829
|
-
return fieldConfig;
|
|
813
|
+
return new GraphQLScalarType2({
|
|
814
|
+
...scalar,
|
|
815
|
+
name: name ?? scalar.name,
|
|
816
|
+
description: (description ?? scalar.description)?.replaceAll("`", "'"),
|
|
817
|
+
serialize(value) {
|
|
818
|
+
if (!errorMessage) {
|
|
819
|
+
return scalar.serialize(value);
|
|
820
|
+
}
|
|
821
|
+
try {
|
|
822
|
+
return scalar.serialize(value);
|
|
823
|
+
} catch (e) {
|
|
824
|
+
logger.error(e);
|
|
825
|
+
throw exception;
|
|
826
|
+
}
|
|
830
827
|
},
|
|
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
|
-
});
|
|
828
|
+
parseValue(value) {
|
|
829
|
+
if (!errorMessage) {
|
|
830
|
+
return scalar.parseValue(value);
|
|
860
831
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
832
|
+
try {
|
|
833
|
+
return scalar.parseValue(value);
|
|
834
|
+
} catch (e) {
|
|
835
|
+
logger.error(e);
|
|
836
|
+
throw exception;
|
|
864
837
|
}
|
|
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;
|
|
838
|
+
},
|
|
839
|
+
parseLiteral(ast, variables) {
|
|
840
|
+
if (!errorMessage) {
|
|
841
|
+
return scalar.parseLiteral(ast, variables);
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
return scalar.parseLiteral(ast, variables);
|
|
845
|
+
} catch (e) {
|
|
846
|
+
logger.error(e);
|
|
847
|
+
throw exception;
|
|
1142
848
|
}
|
|
1143
849
|
}
|
|
1144
850
|
});
|
|
@@ -1328,8 +1034,7 @@ type Subscription {
|
|
|
1328
1034
|
_empty: String
|
|
1329
1035
|
}
|
|
1330
1036
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1037
|
+
${AUTHENTICATED_DIRECTIVE_SDL}
|
|
1333
1038
|
directive @policy(policies: [[String!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
|
|
1334
1039
|
|
|
1335
1040
|
directive @shareable on OBJECT | FIELD_DEFINITION
|
|
@@ -1339,11 +1044,429 @@ directive @link(url: String!, import: [String!]) on SCHEMA
|
|
|
1339
1044
|
`;
|
|
1340
1045
|
// src/upload.ts
|
|
1341
1046
|
import graphqlUploadExpress from "graphql-upload/graphqlUploadExpress.mjs";
|
|
1047
|
+
// src/utils/change-resolvers.utils.ts
|
|
1048
|
+
import {
|
|
1049
|
+
AUDIT_EVENT,
|
|
1050
|
+
CommitType
|
|
1051
|
+
} from "@nxgt/shared-mongo";
|
|
1052
|
+
import { withFilter } from "graphql-subscriptions";
|
|
1053
|
+
function createChangeResolvers(config) {
|
|
1054
|
+
const { entity, collection, service, pubsub } = config;
|
|
1055
|
+
const resolvePubSub = () => typeof pubsub === "function" ? pubsub() : pubsub;
|
|
1056
|
+
return {
|
|
1057
|
+
Query: {
|
|
1058
|
+
[`${entity}Changes`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollection(collection, args),
|
|
1059
|
+
[`${entity}ChangesById`]: async (_parent, args, context) => context.services.audit.findChangesPaginatedByCollectionAndOid(args.id, collection, args)
|
|
1060
|
+
},
|
|
1061
|
+
Subscription: {
|
|
1062
|
+
[`${entity}Changed`]: {
|
|
1063
|
+
resolve: async (payload, _args, context) => ({
|
|
1064
|
+
id: context.services.audit.extractId(payload),
|
|
1065
|
+
data: payload.type !== CommitType.TERMINAL ? await context.services[service].findById(payload.state._id) : null,
|
|
1066
|
+
type: payload.type
|
|
1067
|
+
}),
|
|
1068
|
+
subscribe: withFilter(() => resolvePubSub().subscribe(AUDIT_EVENT), (payload, _variables, context) => context ? context.services.audit.matches(payload, { collection }) : false)
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
// src/utils/codegen.utils.ts
|
|
1074
|
+
var SCALARS_MAPPING = {
|
|
1075
|
+
ID: "any",
|
|
1076
|
+
DateTime: "any",
|
|
1077
|
+
Date: "any",
|
|
1078
|
+
Any: "any",
|
|
1079
|
+
BigInt: "number",
|
|
1080
|
+
EmailAddress: "string",
|
|
1081
|
+
GeoJSON: "any",
|
|
1082
|
+
HSL: "string",
|
|
1083
|
+
HSLA: "string",
|
|
1084
|
+
HexColorCode: "string",
|
|
1085
|
+
Hexadecimal: "string",
|
|
1086
|
+
IBAN: "string",
|
|
1087
|
+
IP: "string",
|
|
1088
|
+
Time: "string",
|
|
1089
|
+
LocalTime: "string",
|
|
1090
|
+
UUID: "string",
|
|
1091
|
+
PositiveInt: "number",
|
|
1092
|
+
NegativeInt: "number",
|
|
1093
|
+
NonPositiveInt: "number",
|
|
1094
|
+
NonNegativeInt: "number",
|
|
1095
|
+
PositiveFloat: "number",
|
|
1096
|
+
NegativeFloat: "number",
|
|
1097
|
+
NonPositiveFloat: "number",
|
|
1098
|
+
NonNegativeFloat: "number",
|
|
1099
|
+
NonEmptyString: "string",
|
|
1100
|
+
PostalCode: "string",
|
|
1101
|
+
RGB: "string",
|
|
1102
|
+
RGBA: "string",
|
|
1103
|
+
SESSN: "string",
|
|
1104
|
+
Timestamp: "string",
|
|
1105
|
+
URL: "string",
|
|
1106
|
+
USCurrency: "string",
|
|
1107
|
+
UTCOffset: "string",
|
|
1108
|
+
Long: "number",
|
|
1109
|
+
Short: "number",
|
|
1110
|
+
Byte: "number",
|
|
1111
|
+
BigDecimal: "string",
|
|
1112
|
+
BigInteger: "string",
|
|
1113
|
+
Char: "string",
|
|
1114
|
+
Locale: "string",
|
|
1115
|
+
CountryCode: "string",
|
|
1116
|
+
CountryName: "string",
|
|
1117
|
+
AccountNumber: "string",
|
|
1118
|
+
CurrencyCode: "string",
|
|
1119
|
+
Duration: "string",
|
|
1120
|
+
Url: "string",
|
|
1121
|
+
JSONObject: "Record<string, any>",
|
|
1122
|
+
JSON: "any",
|
|
1123
|
+
Void: "void",
|
|
1124
|
+
ObjectID: "any",
|
|
1125
|
+
Upload: "File | Blob",
|
|
1126
|
+
Email: "string",
|
|
1127
|
+
Port: "number",
|
|
1128
|
+
IPv4: "string",
|
|
1129
|
+
IPv6: "string",
|
|
1130
|
+
MAC: "string",
|
|
1131
|
+
Password: "string",
|
|
1132
|
+
PhoneNumber: "string",
|
|
1133
|
+
ZIPCode: "string",
|
|
1134
|
+
SSN: "string",
|
|
1135
|
+
GUID: "string",
|
|
1136
|
+
ISBN: "string",
|
|
1137
|
+
Latitude: "number",
|
|
1138
|
+
Longitude: "number",
|
|
1139
|
+
JWT: "string"
|
|
1140
|
+
};
|
|
1141
|
+
// src/utils/data-loader.utils.ts
|
|
1142
|
+
import { cast } from "@nxgt/shared/helpers";
|
|
1143
|
+
import {
|
|
1144
|
+
paginateList
|
|
1145
|
+
} from "@nxgt/shared-mongo";
|
|
1146
|
+
import DataLoader2 from "dataloader";
|
|
1147
|
+
import { isArray } from "lodash";
|
|
1148
|
+
function createDataLoader(batchLoadFn, options) {
|
|
1149
|
+
return new DataLoader2(batchLoadFn, options);
|
|
1150
|
+
}
|
|
1151
|
+
function createConnectionListDataLoader(path, model, options) {
|
|
1152
|
+
return createDataLoader(async (docs) => {
|
|
1153
|
+
const data = await model.find({
|
|
1154
|
+
...options.filter ?? {},
|
|
1155
|
+
_id: docs.flatMap((doc) => {
|
|
1156
|
+
const ids = doc[path] ?? [];
|
|
1157
|
+
return isArray(ids) ? cast(ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))) : [];
|
|
1158
|
+
})
|
|
1159
|
+
}).exec();
|
|
1160
|
+
return cast(docs.map((doc) => model.paginateList(data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString())), options)));
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
function createChildrenConnectionDataLoader(path, model, options) {
|
|
1164
|
+
return createDataLoader(async (docs) => {
|
|
1165
|
+
const filter = {};
|
|
1166
|
+
filter[path] = docs.map((doc) => doc._id);
|
|
1167
|
+
const data = await model.find(filter).exec();
|
|
1168
|
+
return cast(docs.map((doc) => paginateList(data.filter((child) => doc._id?.toString() === child[path]?.toString()), options)));
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
function createSimpleListDataLoader(path, model, options) {
|
|
1172
|
+
return createDataLoader(async (docs) => {
|
|
1173
|
+
const filter = options?.filter ?? {};
|
|
1174
|
+
const ids = docs.flatMap((doc) => {
|
|
1175
|
+
const ids = doc[path] ?? [];
|
|
1176
|
+
return isArray(ids) ? cast(ids) : [];
|
|
1177
|
+
});
|
|
1178
|
+
filter._id = ids;
|
|
1179
|
+
const query = model.find(filter);
|
|
1180
|
+
const data = await query.exec();
|
|
1181
|
+
return cast(docs.map((doc) => {
|
|
1182
|
+
const result = data.filter((child) => cast(doc[path] ?? []).map((item) => item.toString()).includes(child._id.toString()));
|
|
1183
|
+
if (options?.skip && options?.limit) {
|
|
1184
|
+
return result.slice(options.skip, options.skip + options.limit);
|
|
1185
|
+
}
|
|
1186
|
+
if (options?.limit) {
|
|
1187
|
+
return result.slice(0, options.limit);
|
|
1188
|
+
}
|
|
1189
|
+
if (options?.skip) {
|
|
1190
|
+
return result.slice(options.skip);
|
|
1191
|
+
}
|
|
1192
|
+
return result;
|
|
1193
|
+
}));
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
function createChildrenListDataLoader(path, model, options) {
|
|
1197
|
+
return createDataLoader(async (docs) => {
|
|
1198
|
+
const filter = options?.filter ?? {};
|
|
1199
|
+
const key = path;
|
|
1200
|
+
filter[key] = docs.map((doc) => doc._id);
|
|
1201
|
+
const query = model.find(filter);
|
|
1202
|
+
const data = await query.exec();
|
|
1203
|
+
return cast(docs.map((doc) => {
|
|
1204
|
+
const result = data.filter((child) => child[path]?.toString() === doc._id.toString());
|
|
1205
|
+
if (options?.skip && options?.limit) {
|
|
1206
|
+
return result.slice(options.skip, options.skip + options.limit);
|
|
1207
|
+
}
|
|
1208
|
+
if (options?.limit) {
|
|
1209
|
+
return result.slice(0, options.limit);
|
|
1210
|
+
}
|
|
1211
|
+
if (options?.skip) {
|
|
1212
|
+
return result.slice(options.skip);
|
|
1213
|
+
}
|
|
1214
|
+
return result;
|
|
1215
|
+
}));
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
function createSingleChildDataLoader(path, model) {
|
|
1219
|
+
return createDataLoader(async (docs) => {
|
|
1220
|
+
const data = await model.find({
|
|
1221
|
+
_id: docs.map((doc) => cast(doc[path])).filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
|
|
1222
|
+
}).exec();
|
|
1223
|
+
const map = new Map(data.map((doc) => [doc.id, doc]));
|
|
1224
|
+
return docs.map((doc) => map.get(doc[path]?.toString() ?? "") ?? null);
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
function createEntitiesReferenceLoader(model) {
|
|
1228
|
+
return createDataLoader(async (ids) => {
|
|
1229
|
+
const docs = await model.find({
|
|
1230
|
+
_id: ids.filter((item) => !!item && !["undefined", "null"].includes(item?.toString()))
|
|
1231
|
+
}).exec();
|
|
1232
|
+
const map = new Map(docs.map((doc) => [doc.id, doc]));
|
|
1233
|
+
return ids.map((id) => map.get(id) ?? null);
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
// src/utils/errors/create-error.ts
|
|
1237
|
+
import { GraphQLError as GraphQLError3 } from "graphql";
|
|
1238
|
+
function createGraphQLError(message, options) {
|
|
1239
|
+
return new GraphQLError3(message, options);
|
|
1240
|
+
}
|
|
1241
|
+
// src/utils/errors/format-error.ts
|
|
1242
|
+
import {
|
|
1243
|
+
ApolloServerErrorCode,
|
|
1244
|
+
unwrapResolverError
|
|
1245
|
+
} from "@apollo/server/errors";
|
|
1246
|
+
import {
|
|
1247
|
+
translate as translateBase
|
|
1248
|
+
} from "@nxgt/i18n";
|
|
1249
|
+
import { CustomException as CustomException2, ErrorCode as ErrorCode6 } from "@nxgt/shared-exceptions";
|
|
1250
|
+
import { MONGO_UTILS, mongoose } from "@nxgt/shared-mongo";
|
|
1251
|
+
import { kebabCase } from "lodash";
|
|
1252
|
+
function createFormatError(translate = translateBase, production) {
|
|
1253
|
+
return (formattedError, graphQLError) => {
|
|
1254
|
+
if (production && formattedError.extensions?.stacktrace) {
|
|
1255
|
+
formattedError.extensions.stacktrace = undefined;
|
|
1256
|
+
}
|
|
1257
|
+
const error = unwrapResolverError(graphQLError);
|
|
1258
|
+
if (isOryUnavailable(error)) {
|
|
1259
|
+
const unavailable = serviceUnavailableError(error);
|
|
1260
|
+
return {
|
|
1261
|
+
...formattedError,
|
|
1262
|
+
message: unavailable.message,
|
|
1263
|
+
extensions: { ...formattedError.extensions, ...unavailable.extensions }
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
const key = denialMessageKey(error);
|
|
1267
|
+
if (key !== undefined) {
|
|
1268
|
+
return { ...formattedError, message: translate(key) };
|
|
1269
|
+
}
|
|
1270
|
+
if (error instanceof CustomException2 || error instanceof mongoose.MongooseError) {
|
|
1271
|
+
const exception = error instanceof mongoose.MongooseError ? MONGO_UTILS.castError(error) : error;
|
|
1272
|
+
return {
|
|
1273
|
+
...formattedError,
|
|
1274
|
+
message: exception.errorCode === ErrorCode6.ValidationError ? exception.message : translate(exception.message, exception.options),
|
|
1275
|
+
extensions: {
|
|
1276
|
+
...formattedError.extensions,
|
|
1277
|
+
code: exception.errorCode,
|
|
1278
|
+
debugMessage: exception.debugMessage
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
if ([
|
|
1283
|
+
ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED,
|
|
1284
|
+
ApolloServerErrorCode.BAD_REQUEST,
|
|
1285
|
+
ApolloServerErrorCode.BAD_USER_INPUT,
|
|
1286
|
+
ApolloServerErrorCode.GRAPHQL_PARSE_FAILED,
|
|
1287
|
+
ApolloServerErrorCode.OPERATION_RESOLUTION_FAILURE,
|
|
1288
|
+
ApolloServerErrorCode.PERSISTED_QUERY_NOT_FOUND,
|
|
1289
|
+
ApolloServerErrorCode.PERSISTED_QUERY_NOT_SUPPORTED
|
|
1290
|
+
].includes(formattedError.extensions?.code)) {
|
|
1291
|
+
return {
|
|
1292
|
+
...formattedError,
|
|
1293
|
+
message: translate(`errors.${kebabCase(formattedError.extensions?.code)}`, {}),
|
|
1294
|
+
extensions: {
|
|
1295
|
+
...formattedError.extensions,
|
|
1296
|
+
debugMessage: formattedError.message
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
return formattedError;
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
// src/utils/errors/mask-error.ts
|
|
1304
|
+
import {
|
|
1305
|
+
translate as translateBase2
|
|
1306
|
+
} from "@nxgt/i18n";
|
|
1307
|
+
import { CustomException as CustomException3, ErrorCode as ErrorCode7 } from "@nxgt/shared-exceptions";
|
|
1308
|
+
import { MONGO_UTILS as MONGO_UTILS2, mongoose as mongoose2 } from "@nxgt/shared-mongo";
|
|
1309
|
+
import { GraphQLError as GraphQLError4 } from "graphql";
|
|
1310
|
+
function createMaskError(translate = translateBase2) {
|
|
1311
|
+
return (error, message, isDev) => {
|
|
1312
|
+
const original = error instanceof GraphQLError4 && error.originalError ? error.originalError : error;
|
|
1313
|
+
if (isOryUnavailable(original))
|
|
1314
|
+
return serviceUnavailableError(original);
|
|
1315
|
+
const key = denialMessageKey(original);
|
|
1316
|
+
if (key !== undefined) {
|
|
1317
|
+
return new GraphQLError4(translate(key), {
|
|
1318
|
+
...locationOf(error),
|
|
1319
|
+
extensions: original.extensions
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
if (original instanceof CustomException3 || original instanceof mongoose2.MongooseError) {
|
|
1323
|
+
return exceptionError(original, translate);
|
|
1324
|
+
}
|
|
1325
|
+
if (isOriginalGraphQLError(error))
|
|
1326
|
+
return error;
|
|
1327
|
+
return new GraphQLError4(message, {
|
|
1328
|
+
...locationOf(error),
|
|
1329
|
+
extensions: isDev ? {
|
|
1330
|
+
code: ErrorCode7.InternalServerError,
|
|
1331
|
+
debugMessage: String(original)
|
|
1332
|
+
} : { code: ErrorCode7.InternalServerError }
|
|
1333
|
+
});
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
function exceptionError(original, translate) {
|
|
1337
|
+
const exception = original instanceof mongoose2.MongooseError ? MONGO_UTILS2.castError(original) : original;
|
|
1338
|
+
const status = exception.code;
|
|
1339
|
+
return new GraphQLError4(exception.errorCode === ErrorCode7.ValidationError ? exception.message : translate(exception.message, exception.options), {
|
|
1340
|
+
extensions: {
|
|
1341
|
+
code: exception.errorCode,
|
|
1342
|
+
...status ? { http: { status } } : {},
|
|
1343
|
+
...exception.debugMessage ? { debugMessage: exception.debugMessage } : {}
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
function isOriginalGraphQLError(error) {
|
|
1348
|
+
if (!(error instanceof GraphQLError4))
|
|
1349
|
+
return false;
|
|
1350
|
+
return error.originalError == null ? true : isOriginalGraphQLError(error.originalError);
|
|
1351
|
+
}
|
|
1352
|
+
function locationOf(error) {
|
|
1353
|
+
if (!(error instanceof GraphQLError4))
|
|
1354
|
+
return {};
|
|
1355
|
+
return {
|
|
1356
|
+
nodes: error.nodes,
|
|
1357
|
+
source: error.source,
|
|
1358
|
+
positions: error.positions,
|
|
1359
|
+
path: error.path
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
// src/utils/publish.ts
|
|
1363
|
+
import { createRedisEventTarget } from "@graphql-yoga/redis-event-target";
|
|
1364
|
+
import { createPubSub } from "graphql-yoga";
|
|
1365
|
+
var createYogaPubSub = ({
|
|
1366
|
+
publishClient,
|
|
1367
|
+
subscribeClient
|
|
1368
|
+
}) => {
|
|
1369
|
+
const eventTarget = createRedisEventTarget({
|
|
1370
|
+
publishClient,
|
|
1371
|
+
subscribeClient
|
|
1372
|
+
});
|
|
1373
|
+
return createPubSub({ eventTarget });
|
|
1374
|
+
};
|
|
1375
|
+
// src/utils/schema.utils.ts
|
|
1376
|
+
import { existsSync } from "node:fs";
|
|
1377
|
+
import { dirname, join } from "node:path";
|
|
1378
|
+
import { fileURLToPath } from "node:url";
|
|
1379
|
+
import { buildSubgraphSchema as buildSubgraphSchemaBase } from "@apollo/subgraph";
|
|
1380
|
+
import { loadFilesSync } from "@graphql-tools/load-files";
|
|
1381
|
+
import { mergeTypeDefs } from "@graphql-tools/merge";
|
|
1382
|
+
import { pruneSchema } from "@graphql-tools/utils";
|
|
1383
|
+
import { printSchema } from "graphql";
|
|
1384
|
+
function packageRoot() {
|
|
1385
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
1386
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
1387
|
+
const parent = dirname(dir);
|
|
1388
|
+
if (parent === dir)
|
|
1389
|
+
throw new Error("@nxgt/shared-graphql: no package root");
|
|
1390
|
+
dir = parent;
|
|
1391
|
+
}
|
|
1392
|
+
return dir;
|
|
1393
|
+
}
|
|
1394
|
+
var SHARED_SCHEMA_PATH = join(packageRoot(), "graphql/**/*.graphqls");
|
|
1395
|
+
function loadTypeDefs(...paths) {
|
|
1396
|
+
const typeDefs = mergeTypeDefs([
|
|
1397
|
+
...paths.flatMap((path) => loadFilesSync(path, {
|
|
1398
|
+
recursive: true
|
|
1399
|
+
}))
|
|
1400
|
+
]);
|
|
1401
|
+
return typeDefs;
|
|
1402
|
+
}
|
|
1403
|
+
function buildSubgraphSchema(modulesOrSdl) {
|
|
1404
|
+
return pruneSchema(buildSubgraphSchemaBase(modulesOrSdl), {
|
|
1405
|
+
skipUnusedTypesPruning: false
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
async function generateSchema(output, ...paths) {
|
|
1409
|
+
await Bun.write(output, printSchema(buildSubgraphSchema(loadTypeDefs(...paths))));
|
|
1410
|
+
}
|
|
1411
|
+
// src/utils/websocket.ts
|
|
1412
|
+
import { createServer } from "node:http";
|
|
1413
|
+
import { useServer } from "graphql-ws/use/ws";
|
|
1414
|
+
import { WebSocketServer } from "ws";
|
|
1415
|
+
function setupYogaWebSocketServer({
|
|
1416
|
+
yoga,
|
|
1417
|
+
context
|
|
1418
|
+
}) {
|
|
1419
|
+
const httpServer = createServer(yoga);
|
|
1420
|
+
const wsServer = new WebSocketServer({
|
|
1421
|
+
server: httpServer,
|
|
1422
|
+
path: yoga.graphqlEndpoint
|
|
1423
|
+
});
|
|
1424
|
+
useServer({
|
|
1425
|
+
execute: (args) => args.rootValue.execute(args),
|
|
1426
|
+
subscribe: (args) => args.rootValue.subscribe(args),
|
|
1427
|
+
onSubscribe: async (ctx, _id, params) => {
|
|
1428
|
+
const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped({
|
|
1429
|
+
...ctx,
|
|
1430
|
+
req: ctx.extra.request,
|
|
1431
|
+
socket: ctx.extra.socket,
|
|
1432
|
+
params
|
|
1433
|
+
});
|
|
1434
|
+
const args = {
|
|
1435
|
+
schema,
|
|
1436
|
+
operationName: params.operationName,
|
|
1437
|
+
document: parse(params.query),
|
|
1438
|
+
variableValues: params.variables,
|
|
1439
|
+
contextValue: await contextFactory(),
|
|
1440
|
+
rootValue: {
|
|
1441
|
+
execute,
|
|
1442
|
+
subscribe
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
const errors = validate(args.schema, args.document);
|
|
1446
|
+
if (errors.length)
|
|
1447
|
+
return errors;
|
|
1448
|
+
return args;
|
|
1449
|
+
},
|
|
1450
|
+
context
|
|
1451
|
+
}, wsServer);
|
|
1452
|
+
const startServer = (port) => new Promise((resolve) => httpServer.listen({ port }, resolve));
|
|
1453
|
+
return { yoga, httpServer, startServer };
|
|
1454
|
+
}
|
|
1455
|
+
// src/utils/ws-context.ts
|
|
1456
|
+
async function resolveWsUser(connectionParams, authClient) {
|
|
1457
|
+
const rawToken = connectionParams?.authorization ?? connectionParams?.Authorization;
|
|
1458
|
+
const token = rawToken?.replace("Bearer ", "");
|
|
1459
|
+
const { data } = token ? await authClient.POST("/oauth/introspect", { body: { token } }) : { data: null };
|
|
1460
|
+
const user = data?.active ? { ...data, name: data.sub } : undefined;
|
|
1461
|
+
return { user, token };
|
|
1462
|
+
}
|
|
1342
1463
|
export {
|
|
1343
|
-
|
|
1344
|
-
|
|
1464
|
+
AUTHENTICATED_DIRECTIVE_NAME,
|
|
1465
|
+
AUTHENTICATED_DIRECTIVE_SDL,
|
|
1466
|
+
CALLER_TYPES,
|
|
1345
1467
|
DataLoader3 as DataLoader,
|
|
1346
1468
|
FEDERATION_DIRECTIVES,
|
|
1469
|
+
GATEWAY_SECRET_HEADER,
|
|
1347
1470
|
KETO_DIRECTIVES_SDL,
|
|
1348
1471
|
PERMISSION_DIRECTIVE_NAME,
|
|
1349
1472
|
PERMISSION_DIRECTIVE_SDL,
|
|
@@ -1351,6 +1474,7 @@ export {
|
|
|
1351
1474
|
SCALAR_RESOLVERS,
|
|
1352
1475
|
SHARED_SCHEMA_PATH,
|
|
1353
1476
|
SHARED_TYPE_DEFS,
|
|
1477
|
+
applyAuthenticated,
|
|
1354
1478
|
applyKetoChecks,
|
|
1355
1479
|
assertReadablePath,
|
|
1356
1480
|
buildSubgraphSchema,
|
|
@@ -1369,7 +1493,10 @@ export {
|
|
|
1369
1493
|
createSingleChildDataLoader,
|
|
1370
1494
|
createYogaHono,
|
|
1371
1495
|
createYogaPubSub,
|
|
1496
|
+
denial,
|
|
1497
|
+
denialMessageKey,
|
|
1372
1498
|
extractJwtPlugin,
|
|
1499
|
+
gatewaySecret,
|
|
1373
1500
|
generateSchema,
|
|
1374
1501
|
graphqlUploadExpress,
|
|
1375
1502
|
honoYoga,
|
|
@@ -1377,22 +1504,23 @@ export {
|
|
|
1377
1504
|
loadTypeDefs,
|
|
1378
1505
|
objectIds,
|
|
1379
1506
|
oryUnavailableError,
|
|
1380
|
-
|
|
1507
|
+
principalOf,
|
|
1381
1508
|
readPath,
|
|
1382
1509
|
readPermissions,
|
|
1383
|
-
readRequirements,
|
|
1384
1510
|
renderSandbox,
|
|
1511
|
+
requireGatewayTrust,
|
|
1385
1512
|
requireUser,
|
|
1386
1513
|
resolveOryPrincipal,
|
|
1387
1514
|
resolveWsUser,
|
|
1388
|
-
|
|
1515
|
+
sandboxExplorer,
|
|
1389
1516
|
serviceUnavailableError,
|
|
1390
1517
|
setupYogaWebSocketServer,
|
|
1391
1518
|
toPrincipal,
|
|
1392
1519
|
useAuth,
|
|
1520
|
+
useAuthenticated,
|
|
1393
1521
|
useKetoChecks,
|
|
1394
1522
|
useOryAuth
|
|
1395
1523
|
};
|
|
1396
1524
|
|
|
1397
|
-
//# debugId=
|
|
1525
|
+
//# debugId=D1F8FC67079E823264756E2164756E21
|
|
1398
1526
|
//# sourceMappingURL=index.js.map
|