@makerx/graphql-core 3.0.0-beta.5 → 3.0.0-beta.7
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 +5 -3
- package/logging.d.cts +2 -1
- package/logging.d.mts +2 -1
- package/logging.d.ts +2 -1
- package/logging.js +2 -1
- package/logging.js.map +1 -1
- package/logging.mjs +2 -1
- package/logging.mjs.map +1 -1
- package/package.json +2 -2
- package/request-info.d.cts +24 -0
- package/request-info.d.mts +24 -0
- package/request-info.d.ts +24 -0
- package/request-info.js.map +1 -1
- package/request-info.mjs.map +1 -1
- package/shield.d.cts +15 -13
- package/shield.d.mts +15 -13
- package/shield.d.ts +15 -13
- package/shield.js +11 -12
- package/shield.js.map +1 -1
- package/shield.mjs +11 -12
- package/shield.mjs.map +1 -1
- package/subscriptions/server.js +6 -6
- package/subscriptions/server.js.map +1 -1
- package/subscriptions/server.mjs +6 -6
- package/subscriptions/server.mjs.map +1 -1
package/README.md
CHANGED
|
@@ -236,13 +236,15 @@ Convenience builders that read `ctx.user.roles` / `ctx.user.scopes`. All use gra
|
|
|
236
236
|
|
|
237
237
|
### createRule
|
|
238
238
|
|
|
239
|
-
|
|
239
|
+
Strongly typed wrapper around graphql-shield's `rule(...)`. Mirrors the underlying call signature — `createRule(name?, options?)(fn)` — but types `parent`, `args` and `ctx` on `fn` to your generics instead of `any`.
|
|
240
240
|
|
|
241
241
|
```ts
|
|
242
|
-
const isOwner = createRule<GraphQLContext,
|
|
242
|
+
const isOwner = createRule<GraphQLContext, { ownerId: string }>()((parent, _, ctx) => parent.ownerId === ctx.user?.id)
|
|
243
|
+
|
|
244
|
+
const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })((_, __, ctx) => ctx.user?.roles.includes('admin') === true)
|
|
243
245
|
```
|
|
244
246
|
|
|
245
|
-
|
|
247
|
+
Generics are `<TContext, TParent = unknown, TArgs = unknown>`. The `name` and `options` arguments are forwarded verbatim to graphql-shield's `rule`.
|
|
246
248
|
|
|
247
249
|
### combineRuleWithAll
|
|
248
250
|
|
package/logging.d.cts
CHANGED
|
@@ -71,7 +71,8 @@ export interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extend
|
|
|
71
71
|
* Top level and result entries with null or undefined values will be omitted.
|
|
72
72
|
*/
|
|
73
73
|
export declare const logGraphQLOperation: <TLogger extends Logger = Logger>({ message, started, type, operationName, query, variables, result, logger, logLevel, ...rest }: GraphQLLogOperationInfo<TLogger>) => void;
|
|
74
|
-
export declare const logSubscriptionOperation: <TLogger extends Logger = Logger>({ args, result, message, logLevel, resolveLogger, }: {
|
|
74
|
+
export declare const logSubscriptionOperation: <TLogger extends Logger = Logger>({ id, args, result, message, logLevel, resolveLogger, }: {
|
|
75
|
+
id?: string;
|
|
75
76
|
args: ExecutionArgs;
|
|
76
77
|
result?: ExecutionResult;
|
|
77
78
|
message?: string;
|
package/logging.d.mts
CHANGED
|
@@ -71,7 +71,8 @@ export interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extend
|
|
|
71
71
|
* Top level and result entries with null or undefined values will be omitted.
|
|
72
72
|
*/
|
|
73
73
|
export declare const logGraphQLOperation: <TLogger extends Logger = Logger>({ message, started, type, operationName, query, variables, result, logger, logLevel, ...rest }: GraphQLLogOperationInfo<TLogger>) => void;
|
|
74
|
-
export declare const logSubscriptionOperation: <TLogger extends Logger = Logger>({ args, result, message, logLevel, resolveLogger, }: {
|
|
74
|
+
export declare const logSubscriptionOperation: <TLogger extends Logger = Logger>({ id, args, result, message, logLevel, resolveLogger, }: {
|
|
75
|
+
id?: string;
|
|
75
76
|
args: ExecutionArgs;
|
|
76
77
|
result?: ExecutionResult;
|
|
77
78
|
message?: string;
|
package/logging.d.ts
CHANGED
|
@@ -71,7 +71,8 @@ export interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extend
|
|
|
71
71
|
* Top level and result entries with null or undefined values will be omitted.
|
|
72
72
|
*/
|
|
73
73
|
export declare const logGraphQLOperation: <TLogger extends Logger = Logger>({ message, started, type, operationName, query, variables, result, logger, logLevel, ...rest }: GraphQLLogOperationInfo<TLogger>) => void;
|
|
74
|
-
export declare const logSubscriptionOperation: <TLogger extends Logger = Logger>({ args, result, message, logLevel, resolveLogger, }: {
|
|
74
|
+
export declare const logSubscriptionOperation: <TLogger extends Logger = Logger>({ id, args, result, message, logLevel, resolveLogger, }: {
|
|
75
|
+
id?: string;
|
|
75
76
|
args: ExecutionArgs;
|
|
76
77
|
result?: ExecutionResult;
|
|
77
78
|
message?: string;
|
package/logging.js
CHANGED
|
@@ -24,7 +24,7 @@ const logGraphQLOperation = ({ message = 'GraphQL operation', started, type, ope
|
|
|
24
24
|
...compat.omitBy(rest, utils.isNil),
|
|
25
25
|
}, utils.isNil));
|
|
26
26
|
};
|
|
27
|
-
const logSubscriptionOperation = ({ args, result, message, logLevel, resolveLogger, }) => {
|
|
27
|
+
const logSubscriptionOperation = ({ id, args, result, message, logLevel, resolveLogger, }) => {
|
|
28
28
|
const logger = resolveLogger
|
|
29
29
|
? resolveLogger(args.contextValue)
|
|
30
30
|
: args.contextValue.logger;
|
|
@@ -35,6 +35,7 @@ const logSubscriptionOperation = ({ args, result, message, logLevel, resolveLogg
|
|
|
35
35
|
logGraphQLOperation({
|
|
36
36
|
message,
|
|
37
37
|
type: graphql.OperationTypeNode.SUBSCRIPTION,
|
|
38
|
+
subscriptionId: id,
|
|
38
39
|
operationName,
|
|
39
40
|
query: graphql.print(document),
|
|
40
41
|
variables: variableValues,
|
package/logging.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logging.js","sources":["../src/logging.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { isLocalDev } from '@makerx/node-common'\nimport type { ExecutionArgs, GraphQLFormattedError } from 'graphql'\nimport { OperationTypeNode, print } from 'graphql'\nimport type { ExecutionResult } from 'graphql-ws'\nimport { omitBy } from 'es-toolkit/compat'\nimport type { GraphQLContext } from './context'\nimport { isIntrospectionQuery, isNil } from './utils'\n\ntype LogFunction = Logger['info']\nexport type LoggerLogFunctions<T extends Logger> = {\n [Property in keyof T]: LogFunction\n}\n\n/**\n * Info for logging a GraphQL operation in a consistent format with the option of including any additional data.\n */\nexport interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extends Record<string, unknown> {\n /**\n * The message to log, defaults to 'GraphQL operation'.\n */\n message?: string\n /**\n * The timestamp when the operation started, if supplied, the duration will be logged.\n */\n started?: number\n /**\n * The type of GraphQL operation.\n */\n type?: OperationTypeNode | null\n /**\n * The name of the GraphQL operation.\n */\n operationName?: string | null\n /**\n * The formatted GraphQL query.\n */\n query?: string | null\n /**\n * The GraphQL variables.\n */\n variables?: Record<string, unknown> | null\n /**\n * The result of the GraphQL operation.\n * Generally, we don't log the data or extensions, just errors.\n */\n result?: {\n data?: Record<string, unknown> | null\n errors?: readonly GraphQLFormattedError[] | null\n extensions?: Record<string, unknown> | null\n hasNext?: boolean\n }\n /**\n * Whether the operation is an introspection query.\n */\n isIntrospectionQuery?: boolean\n /**\n * Whether the operation is part of an incremental response.\n */\n isIncrementalResponse?: boolean\n /**\n * Whether the operation is a subsequent payload of an incremental response.\n */\n isSubsequentPayload?: boolean\n /**\n * The logger to use.\n */\n logger: TLogger\n /**\n * The logger function to use, defaults to 'info'.\n */\n logLevel?: keyof LoggerLogFunctions<TLogger>\n}\n\n/**\n * Logs a GraphQL operation in a consistent format with the option of including any additional data.\n * Top level and result entries with null or undefined values will be omitted.\n */\nexport const logGraphQLOperation = <TLogger extends Logger = Logger>({\n message = 'GraphQL operation',\n started,\n type,\n operationName,\n query,\n variables,\n result,\n logger,\n logLevel = 'info',\n ...rest\n}: GraphQLLogOperationInfo<TLogger>) => {\n const isIntrospection = query && isIntrospectionQuery(query)\n if (isLocalDev && isIntrospection) return\n logger[logLevel as keyof Logger](\n message,\n omitBy(\n {\n type,\n operationName,\n query,\n variables: variables && Object.keys(variables).length > 0 ? variables : undefined,\n duration: started ? Date.now() - started : undefined,\n result: result ? omitBy(result, isNil) : undefined,\n isIntrospectionQuery: isIntrospection || undefined,\n ...omitBy(rest, isNil),\n },\n isNil,\n ),\n )\n}\n\nexport const logSubscriptionOperation = <TLogger extends Logger = Logger>({\n args,\n result,\n message,\n logLevel,\n resolveLogger,\n}: {\n args: ExecutionArgs\n result?: ExecutionResult\n message?: string\n logLevel?: keyof LoggerLogFunctions<TLogger>\n resolveLogger?: (context: GraphQLContext) => TLogger\n}) => {\n const logger = resolveLogger\n ? resolveLogger(args.contextValue as GraphQLContext)\n : ((args.contextValue as GraphQLContext).logger as TLogger)\n if (!logger) return\n\n const { operationName, variableValues, document } = args\n const { data, ...resultWithoutData } = result ?? {}\n\n logGraphQLOperation({\n message,\n type: OperationTypeNode.SUBSCRIPTION,\n operationName,\n query: print(document),\n variables: variableValues,\n result: resultWithoutData,\n logger,\n logLevel,\n })\n}\n"],"names":["isIntrospectionQuery","isLocalDev","omitBy","isNil","OperationTypeNode","print"],"mappings":";;;;;;;AA0EA;;;AAGG;AACI,MAAM,mBAAmB,GAAG,CAAkC,EACnE,OAAO,GAAG,mBAAmB,EAC7B,OAAO,EACP,IAAI,EACJ,aAAa,EACb,KAAK,EACL,SAAS,EACT,MAAM,EACN,MAAM,EACN,QAAQ,GAAG,MAAM,EACjB,GAAG,IAAI,EAC0B,KAAI;IACrC,MAAM,eAAe,GAAG,KAAK,IAAIA,0BAAoB,CAAC,KAAK,CAAC;IAC5D,IAAIC,qBAAU,IAAI,eAAe;QAAE;AACnC,IAAA,MAAM,CAAC,QAAwB,CAAC,CAC9B,OAAO,EACPC,aAAM,CACJ;QACE,IAAI;QACJ,aAAa;QACb,KAAK;QACL,SAAS,EAAE,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,SAAS,GAAG,SAAS;AACjF,QAAA,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,SAAS;AACpD,QAAA,MAAM,EAAE,MAAM,GAAGA,aAAM,CAAC,MAAM,EAAEC,WAAK,CAAC,GAAG,SAAS;QAClD,oBAAoB,EAAE,eAAe,IAAI,SAAS;AAClD,QAAA,GAAGD,aAAM,CAAC,IAAI,EAAEC,WAAK,CAAC;KACvB,EACDA,WAAK,CACN,CACF;AACH;AAEO,MAAM,wBAAwB,GAAG,CAAkC,EACxE,IAAI,EACJ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,
|
|
1
|
+
{"version":3,"file":"logging.js","sources":["../src/logging.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { isLocalDev } from '@makerx/node-common'\nimport type { ExecutionArgs, GraphQLFormattedError } from 'graphql'\nimport { OperationTypeNode, print } from 'graphql'\nimport type { ExecutionResult } from 'graphql-ws'\nimport { omitBy } from 'es-toolkit/compat'\nimport type { GraphQLContext } from './context'\nimport { isIntrospectionQuery, isNil } from './utils'\n\ntype LogFunction = Logger['info']\nexport type LoggerLogFunctions<T extends Logger> = {\n [Property in keyof T]: LogFunction\n}\n\n/**\n * Info for logging a GraphQL operation in a consistent format with the option of including any additional data.\n */\nexport interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extends Record<string, unknown> {\n /**\n * The message to log, defaults to 'GraphQL operation'.\n */\n message?: string\n /**\n * The timestamp when the operation started, if supplied, the duration will be logged.\n */\n started?: number\n /**\n * The type of GraphQL operation.\n */\n type?: OperationTypeNode | null\n /**\n * The name of the GraphQL operation.\n */\n operationName?: string | null\n /**\n * The formatted GraphQL query.\n */\n query?: string | null\n /**\n * The GraphQL variables.\n */\n variables?: Record<string, unknown> | null\n /**\n * The result of the GraphQL operation.\n * Generally, we don't log the data or extensions, just errors.\n */\n result?: {\n data?: Record<string, unknown> | null\n errors?: readonly GraphQLFormattedError[] | null\n extensions?: Record<string, unknown> | null\n hasNext?: boolean\n }\n /**\n * Whether the operation is an introspection query.\n */\n isIntrospectionQuery?: boolean\n /**\n * Whether the operation is part of an incremental response.\n */\n isIncrementalResponse?: boolean\n /**\n * Whether the operation is a subsequent payload of an incremental response.\n */\n isSubsequentPayload?: boolean\n /**\n * The logger to use.\n */\n logger: TLogger\n /**\n * The logger function to use, defaults to 'info'.\n */\n logLevel?: keyof LoggerLogFunctions<TLogger>\n}\n\n/**\n * Logs a GraphQL operation in a consistent format with the option of including any additional data.\n * Top level and result entries with null or undefined values will be omitted.\n */\nexport const logGraphQLOperation = <TLogger extends Logger = Logger>({\n message = 'GraphQL operation',\n started,\n type,\n operationName,\n query,\n variables,\n result,\n logger,\n logLevel = 'info',\n ...rest\n}: GraphQLLogOperationInfo<TLogger>) => {\n const isIntrospection = query && isIntrospectionQuery(query)\n if (isLocalDev && isIntrospection) return\n logger[logLevel as keyof Logger](\n message,\n omitBy(\n {\n type,\n operationName,\n query,\n variables: variables && Object.keys(variables).length > 0 ? variables : undefined,\n duration: started ? Date.now() - started : undefined,\n result: result ? omitBy(result, isNil) : undefined,\n isIntrospectionQuery: isIntrospection || undefined,\n ...omitBy(rest, isNil),\n },\n isNil,\n ),\n )\n}\n\nexport const logSubscriptionOperation = <TLogger extends Logger = Logger>({\n id,\n args,\n result,\n message,\n logLevel,\n resolveLogger,\n}: {\n id?: string\n args: ExecutionArgs\n result?: ExecutionResult\n message?: string\n logLevel?: keyof LoggerLogFunctions<TLogger>\n resolveLogger?: (context: GraphQLContext) => TLogger\n}) => {\n const logger = resolveLogger\n ? resolveLogger(args.contextValue as GraphQLContext)\n : ((args.contextValue as GraphQLContext).logger as TLogger)\n if (!logger) return\n\n const { operationName, variableValues, document } = args\n const { data, ...resultWithoutData } = result ?? {}\n\n logGraphQLOperation({\n message,\n type: OperationTypeNode.SUBSCRIPTION,\n subscriptionId: id,\n operationName,\n query: print(document),\n variables: variableValues,\n result: resultWithoutData,\n logger,\n logLevel,\n })\n}\n"],"names":["isIntrospectionQuery","isLocalDev","omitBy","isNil","OperationTypeNode","print"],"mappings":";;;;;;;AA0EA;;;AAGG;AACI,MAAM,mBAAmB,GAAG,CAAkC,EACnE,OAAO,GAAG,mBAAmB,EAC7B,OAAO,EACP,IAAI,EACJ,aAAa,EACb,KAAK,EACL,SAAS,EACT,MAAM,EACN,MAAM,EACN,QAAQ,GAAG,MAAM,EACjB,GAAG,IAAI,EAC0B,KAAI;IACrC,MAAM,eAAe,GAAG,KAAK,IAAIA,0BAAoB,CAAC,KAAK,CAAC;IAC5D,IAAIC,qBAAU,IAAI,eAAe;QAAE;AACnC,IAAA,MAAM,CAAC,QAAwB,CAAC,CAC9B,OAAO,EACPC,aAAM,CACJ;QACE,IAAI;QACJ,aAAa;QACb,KAAK;QACL,SAAS,EAAE,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,SAAS,GAAG,SAAS;AACjF,QAAA,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,SAAS;AACpD,QAAA,MAAM,EAAE,MAAM,GAAGA,aAAM,CAAC,MAAM,EAAEC,WAAK,CAAC,GAAG,SAAS;QAClD,oBAAoB,EAAE,eAAe,IAAI,SAAS;AAClD,QAAA,GAAGD,aAAM,CAAC,IAAI,EAAEC,WAAK,CAAC;KACvB,EACDA,WAAK,CACN,CACF;AACH;AAEO,MAAM,wBAAwB,GAAG,CAAkC,EACxE,EAAE,EACF,IAAI,EACJ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,GAQd,KAAI;IACH,MAAM,MAAM,GAAG;AACb,UAAE,aAAa,CAAC,IAAI,CAAC,YAA8B;AACnD,UAAI,IAAI,CAAC,YAA+B,CAAC,MAAkB;AAC7D,IAAA,IAAI,CAAC,MAAM;QAAE;IAEb,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,IAAI;IACxD,MAAM,EAAE,IAAI,EAAE,GAAG,iBAAiB,EAAE,GAAG,MAAM,IAAI,EAAE;AAEnD,IAAA,mBAAmB,CAAC;QAClB,OAAO;QACP,IAAI,EAAEC,yBAAiB,CAAC,YAAY;AACpC,QAAA,cAAc,EAAE,EAAE;QAClB,aAAa;AACb,QAAA,KAAK,EAAEC,aAAK,CAAC,QAAQ,CAAC;AACtB,QAAA,SAAS,EAAE,cAAc;AACzB,QAAA,MAAM,EAAE,iBAAiB;QACzB,MAAM;QACN,QAAQ;AACT,KAAA,CAAC;AACJ;;;;;"}
|
package/logging.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const logGraphQLOperation = ({ message = 'GraphQL operation', started, type, ope
|
|
|
22
22
|
...omitBy(rest, isNil),
|
|
23
23
|
}, isNil));
|
|
24
24
|
};
|
|
25
|
-
const logSubscriptionOperation = ({ args, result, message, logLevel, resolveLogger, }) => {
|
|
25
|
+
const logSubscriptionOperation = ({ id, args, result, message, logLevel, resolveLogger, }) => {
|
|
26
26
|
const logger = resolveLogger
|
|
27
27
|
? resolveLogger(args.contextValue)
|
|
28
28
|
: args.contextValue.logger;
|
|
@@ -33,6 +33,7 @@ const logSubscriptionOperation = ({ args, result, message, logLevel, resolveLogg
|
|
|
33
33
|
logGraphQLOperation({
|
|
34
34
|
message,
|
|
35
35
|
type: OperationTypeNode.SUBSCRIPTION,
|
|
36
|
+
subscriptionId: id,
|
|
36
37
|
operationName,
|
|
37
38
|
query: print(document),
|
|
38
39
|
variables: variableValues,
|
package/logging.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logging.mjs","sources":["../src/logging.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { isLocalDev } from '@makerx/node-common'\nimport type { ExecutionArgs, GraphQLFormattedError } from 'graphql'\nimport { OperationTypeNode, print } from 'graphql'\nimport type { ExecutionResult } from 'graphql-ws'\nimport { omitBy } from 'es-toolkit/compat'\nimport type { GraphQLContext } from './context'\nimport { isIntrospectionQuery, isNil } from './utils'\n\ntype LogFunction = Logger['info']\nexport type LoggerLogFunctions<T extends Logger> = {\n [Property in keyof T]: LogFunction\n}\n\n/**\n * Info for logging a GraphQL operation in a consistent format with the option of including any additional data.\n */\nexport interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extends Record<string, unknown> {\n /**\n * The message to log, defaults to 'GraphQL operation'.\n */\n message?: string\n /**\n * The timestamp when the operation started, if supplied, the duration will be logged.\n */\n started?: number\n /**\n * The type of GraphQL operation.\n */\n type?: OperationTypeNode | null\n /**\n * The name of the GraphQL operation.\n */\n operationName?: string | null\n /**\n * The formatted GraphQL query.\n */\n query?: string | null\n /**\n * The GraphQL variables.\n */\n variables?: Record<string, unknown> | null\n /**\n * The result of the GraphQL operation.\n * Generally, we don't log the data or extensions, just errors.\n */\n result?: {\n data?: Record<string, unknown> | null\n errors?: readonly GraphQLFormattedError[] | null\n extensions?: Record<string, unknown> | null\n hasNext?: boolean\n }\n /**\n * Whether the operation is an introspection query.\n */\n isIntrospectionQuery?: boolean\n /**\n * Whether the operation is part of an incremental response.\n */\n isIncrementalResponse?: boolean\n /**\n * Whether the operation is a subsequent payload of an incremental response.\n */\n isSubsequentPayload?: boolean\n /**\n * The logger to use.\n */\n logger: TLogger\n /**\n * The logger function to use, defaults to 'info'.\n */\n logLevel?: keyof LoggerLogFunctions<TLogger>\n}\n\n/**\n * Logs a GraphQL operation in a consistent format with the option of including any additional data.\n * Top level and result entries with null or undefined values will be omitted.\n */\nexport const logGraphQLOperation = <TLogger extends Logger = Logger>({\n message = 'GraphQL operation',\n started,\n type,\n operationName,\n query,\n variables,\n result,\n logger,\n logLevel = 'info',\n ...rest\n}: GraphQLLogOperationInfo<TLogger>) => {\n const isIntrospection = query && isIntrospectionQuery(query)\n if (isLocalDev && isIntrospection) return\n logger[logLevel as keyof Logger](\n message,\n omitBy(\n {\n type,\n operationName,\n query,\n variables: variables && Object.keys(variables).length > 0 ? variables : undefined,\n duration: started ? Date.now() - started : undefined,\n result: result ? omitBy(result, isNil) : undefined,\n isIntrospectionQuery: isIntrospection || undefined,\n ...omitBy(rest, isNil),\n },\n isNil,\n ),\n )\n}\n\nexport const logSubscriptionOperation = <TLogger extends Logger = Logger>({\n args,\n result,\n message,\n logLevel,\n resolveLogger,\n}: {\n args: ExecutionArgs\n result?: ExecutionResult\n message?: string\n logLevel?: keyof LoggerLogFunctions<TLogger>\n resolveLogger?: (context: GraphQLContext) => TLogger\n}) => {\n const logger = resolveLogger\n ? resolveLogger(args.contextValue as GraphQLContext)\n : ((args.contextValue as GraphQLContext).logger as TLogger)\n if (!logger) return\n\n const { operationName, variableValues, document } = args\n const { data, ...resultWithoutData } = result ?? {}\n\n logGraphQLOperation({\n message,\n type: OperationTypeNode.SUBSCRIPTION,\n operationName,\n query: print(document),\n variables: variableValues,\n result: resultWithoutData,\n logger,\n logLevel,\n })\n}\n"],"names":[],"mappings":";;;;;AA0EA;;;AAGG;AACI,MAAM,mBAAmB,GAAG,CAAkC,EACnE,OAAO,GAAG,mBAAmB,EAC7B,OAAO,EACP,IAAI,EACJ,aAAa,EACb,KAAK,EACL,SAAS,EACT,MAAM,EACN,MAAM,EACN,QAAQ,GAAG,MAAM,EACjB,GAAG,IAAI,EAC0B,KAAI;IACrC,MAAM,eAAe,GAAG,KAAK,IAAI,oBAAoB,CAAC,KAAK,CAAC;IAC5D,IAAI,UAAU,IAAI,eAAe;QAAE;AACnC,IAAA,MAAM,CAAC,QAAwB,CAAC,CAC9B,OAAO,EACP,MAAM,CACJ;QACE,IAAI;QACJ,aAAa;QACb,KAAK;QACL,SAAS,EAAE,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,SAAS,GAAG,SAAS;AACjF,QAAA,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,SAAS;AACpD,QAAA,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,SAAS;QAClD,oBAAoB,EAAE,eAAe,IAAI,SAAS;AAClD,QAAA,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC;KACvB,EACD,KAAK,CACN,CACF;AACH;AAEO,MAAM,wBAAwB,GAAG,CAAkC,EACxE,IAAI,EACJ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,
|
|
1
|
+
{"version":3,"file":"logging.mjs","sources":["../src/logging.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { isLocalDev } from '@makerx/node-common'\nimport type { ExecutionArgs, GraphQLFormattedError } from 'graphql'\nimport { OperationTypeNode, print } from 'graphql'\nimport type { ExecutionResult } from 'graphql-ws'\nimport { omitBy } from 'es-toolkit/compat'\nimport type { GraphQLContext } from './context'\nimport { isIntrospectionQuery, isNil } from './utils'\n\ntype LogFunction = Logger['info']\nexport type LoggerLogFunctions<T extends Logger> = {\n [Property in keyof T]: LogFunction\n}\n\n/**\n * Info for logging a GraphQL operation in a consistent format with the option of including any additional data.\n */\nexport interface GraphQLLogOperationInfo<TLogger extends Logger = Logger> extends Record<string, unknown> {\n /**\n * The message to log, defaults to 'GraphQL operation'.\n */\n message?: string\n /**\n * The timestamp when the operation started, if supplied, the duration will be logged.\n */\n started?: number\n /**\n * The type of GraphQL operation.\n */\n type?: OperationTypeNode | null\n /**\n * The name of the GraphQL operation.\n */\n operationName?: string | null\n /**\n * The formatted GraphQL query.\n */\n query?: string | null\n /**\n * The GraphQL variables.\n */\n variables?: Record<string, unknown> | null\n /**\n * The result of the GraphQL operation.\n * Generally, we don't log the data or extensions, just errors.\n */\n result?: {\n data?: Record<string, unknown> | null\n errors?: readonly GraphQLFormattedError[] | null\n extensions?: Record<string, unknown> | null\n hasNext?: boolean\n }\n /**\n * Whether the operation is an introspection query.\n */\n isIntrospectionQuery?: boolean\n /**\n * Whether the operation is part of an incremental response.\n */\n isIncrementalResponse?: boolean\n /**\n * Whether the operation is a subsequent payload of an incremental response.\n */\n isSubsequentPayload?: boolean\n /**\n * The logger to use.\n */\n logger: TLogger\n /**\n * The logger function to use, defaults to 'info'.\n */\n logLevel?: keyof LoggerLogFunctions<TLogger>\n}\n\n/**\n * Logs a GraphQL operation in a consistent format with the option of including any additional data.\n * Top level and result entries with null or undefined values will be omitted.\n */\nexport const logGraphQLOperation = <TLogger extends Logger = Logger>({\n message = 'GraphQL operation',\n started,\n type,\n operationName,\n query,\n variables,\n result,\n logger,\n logLevel = 'info',\n ...rest\n}: GraphQLLogOperationInfo<TLogger>) => {\n const isIntrospection = query && isIntrospectionQuery(query)\n if (isLocalDev && isIntrospection) return\n logger[logLevel as keyof Logger](\n message,\n omitBy(\n {\n type,\n operationName,\n query,\n variables: variables && Object.keys(variables).length > 0 ? variables : undefined,\n duration: started ? Date.now() - started : undefined,\n result: result ? omitBy(result, isNil) : undefined,\n isIntrospectionQuery: isIntrospection || undefined,\n ...omitBy(rest, isNil),\n },\n isNil,\n ),\n )\n}\n\nexport const logSubscriptionOperation = <TLogger extends Logger = Logger>({\n id,\n args,\n result,\n message,\n logLevel,\n resolveLogger,\n}: {\n id?: string\n args: ExecutionArgs\n result?: ExecutionResult\n message?: string\n logLevel?: keyof LoggerLogFunctions<TLogger>\n resolveLogger?: (context: GraphQLContext) => TLogger\n}) => {\n const logger = resolveLogger\n ? resolveLogger(args.contextValue as GraphQLContext)\n : ((args.contextValue as GraphQLContext).logger as TLogger)\n if (!logger) return\n\n const { operationName, variableValues, document } = args\n const { data, ...resultWithoutData } = result ?? {}\n\n logGraphQLOperation({\n message,\n type: OperationTypeNode.SUBSCRIPTION,\n subscriptionId: id,\n operationName,\n query: print(document),\n variables: variableValues,\n result: resultWithoutData,\n logger,\n logLevel,\n })\n}\n"],"names":[],"mappings":";;;;;AA0EA;;;AAGG;AACI,MAAM,mBAAmB,GAAG,CAAkC,EACnE,OAAO,GAAG,mBAAmB,EAC7B,OAAO,EACP,IAAI,EACJ,aAAa,EACb,KAAK,EACL,SAAS,EACT,MAAM,EACN,MAAM,EACN,QAAQ,GAAG,MAAM,EACjB,GAAG,IAAI,EAC0B,KAAI;IACrC,MAAM,eAAe,GAAG,KAAK,IAAI,oBAAoB,CAAC,KAAK,CAAC;IAC5D,IAAI,UAAU,IAAI,eAAe;QAAE;AACnC,IAAA,MAAM,CAAC,QAAwB,CAAC,CAC9B,OAAO,EACP,MAAM,CACJ;QACE,IAAI;QACJ,aAAa;QACb,KAAK;QACL,SAAS,EAAE,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,SAAS,GAAG,SAAS;AACjF,QAAA,QAAQ,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,SAAS;AACpD,QAAA,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,SAAS;QAClD,oBAAoB,EAAE,eAAe,IAAI,SAAS;AAClD,QAAA,GAAG,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC;KACvB,EACD,KAAK,CACN,CACF;AACH;AAEO,MAAM,wBAAwB,GAAG,CAAkC,EACxE,EAAE,EACF,IAAI,EACJ,MAAM,EACN,OAAO,EACP,QAAQ,EACR,aAAa,GAQd,KAAI;IACH,MAAM,MAAM,GAAG;AACb,UAAE,aAAa,CAAC,IAAI,CAAC,YAA8B;AACnD,UAAI,IAAI,CAAC,YAA+B,CAAC,MAAkB;AAC7D,IAAA,IAAI,CAAC,MAAM;QAAE;IAEb,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,GAAG,IAAI;IACxD,MAAM,EAAE,IAAI,EAAE,GAAG,iBAAiB,EAAE,GAAG,MAAM,IAAI,EAAE;AAEnD,IAAA,mBAAmB,CAAC;QAClB,OAAO;QACP,IAAI,EAAE,iBAAiB,CAAC,YAAY;AACpC,QAAA,cAAc,EAAE,EAAE;QAClB,aAAa;AACb,QAAA,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC;AACtB,QAAA,SAAS,EAAE,cAAc;AACzB,QAAA,MAAM,EAAE,iBAAiB;QACzB,MAAM;QACN,QAAQ;AACT,KAAA,CAAC;AACJ;;;;"}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"**"
|
|
5
5
|
],
|
|
6
6
|
"name": "@makerx/graphql-core",
|
|
7
|
-
"version": "3.0.0-beta.
|
|
7
|
+
"version": "3.0.0-beta.7",
|
|
8
8
|
"private": false,
|
|
9
9
|
"description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs",
|
|
10
10
|
"author": "MakerX",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"express": ">=4",
|
|
27
27
|
"graphql": ">=16",
|
|
28
28
|
"graphql-shield": ">=7",
|
|
29
|
-
"graphql-ws": ">=
|
|
29
|
+
"graphql-ws": ">=6",
|
|
30
30
|
"ws": ">=8"
|
|
31
31
|
},
|
|
32
32
|
"peerDependenciesMeta": {
|
package/request-info.d.cts
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
import type { Request } from 'express';
|
|
2
2
|
import type { IncomingMessage } from 'http';
|
|
3
|
+
/**
|
|
4
|
+
* Normalised metadata describing an inbound HTTP request or WebSocket upgrade, used as a
|
|
5
|
+
* common shape for logging, tracing, and request-scoped context across HTTP and subscription
|
|
6
|
+
* transports. Extends `Record<string, unknown>` so consumers can augment it with additional
|
|
7
|
+
* fields without losing type compatibility.
|
|
8
|
+
*/
|
|
3
9
|
export interface BaseRequestInfo extends Record<string, unknown> {
|
|
10
|
+
/** Unique identifier for the request, taken from the `x-request-id` header or generated as a UUID. */
|
|
4
11
|
requestId: string;
|
|
12
|
+
/** Transport that produced the request: `http` for Express requests, `subscription` for WebSocket upgrades. */
|
|
5
13
|
source: 'http' | 'subscription';
|
|
14
|
+
/** Resolved scheme of the request, accounting for TLS termination and `x-forwarded-proto`. */
|
|
6
15
|
protocol: 'http' | 'https' | 'ws' | 'wss';
|
|
16
|
+
/** Hostname resolved from `x-forwarded-host`, the `host` header, or the Express hostname fallback. */
|
|
7
17
|
host: string;
|
|
18
|
+
/** Port resolved from the host header, omitted when it matches the default for the protocol. */
|
|
8
19
|
port?: number;
|
|
20
|
+
/** HTTP method (e.g. `GET`, `POST`); empty string when not provided on the underlying request. */
|
|
9
21
|
method: string;
|
|
22
|
+
/** Base URL in the form `protocol://host[:port]`, with default ports omitted. */
|
|
10
23
|
baseUrl: string;
|
|
24
|
+
/**
|
|
25
|
+
* Request path with query string (not an absolute URL), e.g. `/graphql?op=Foo`. Sourced from
|
|
26
|
+
* Express `req.originalUrl` for HTTP (preserved across router mounts/rewrites) or raw `req.url`
|
|
27
|
+
* for WebSocket upgrades. Combine with `baseUrl` to form an absolute URL.
|
|
28
|
+
*/
|
|
11
29
|
url: string;
|
|
30
|
+
/** Value of the `origin` header, or empty string when absent. */
|
|
12
31
|
origin: string;
|
|
32
|
+
/** Value of the `referer` header, when present. */
|
|
13
33
|
referer?: string;
|
|
34
|
+
/** Value of the `x-correlation-id` header for cross-service request correlation, when present. */
|
|
14
35
|
correlationId?: string;
|
|
36
|
+
/** Azure Application Request Routing log id from the `x-arr-log-id` header, when present. */
|
|
15
37
|
arrLogId?: string;
|
|
38
|
+
/** Client IP from the first `x-forwarded-for` entry, falling back to the socket remote address. */
|
|
16
39
|
clientIp?: string;
|
|
40
|
+
/** Value of the `user-agent` header, when present. */
|
|
17
41
|
userAgent?: string;
|
|
18
42
|
}
|
|
19
43
|
/**
|
package/request-info.d.mts
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
import type { Request } from 'express';
|
|
2
2
|
import type { IncomingMessage } from 'http';
|
|
3
|
+
/**
|
|
4
|
+
* Normalised metadata describing an inbound HTTP request or WebSocket upgrade, used as a
|
|
5
|
+
* common shape for logging, tracing, and request-scoped context across HTTP and subscription
|
|
6
|
+
* transports. Extends `Record<string, unknown>` so consumers can augment it with additional
|
|
7
|
+
* fields without losing type compatibility.
|
|
8
|
+
*/
|
|
3
9
|
export interface BaseRequestInfo extends Record<string, unknown> {
|
|
10
|
+
/** Unique identifier for the request, taken from the `x-request-id` header or generated as a UUID. */
|
|
4
11
|
requestId: string;
|
|
12
|
+
/** Transport that produced the request: `http` for Express requests, `subscription` for WebSocket upgrades. */
|
|
5
13
|
source: 'http' | 'subscription';
|
|
14
|
+
/** Resolved scheme of the request, accounting for TLS termination and `x-forwarded-proto`. */
|
|
6
15
|
protocol: 'http' | 'https' | 'ws' | 'wss';
|
|
16
|
+
/** Hostname resolved from `x-forwarded-host`, the `host` header, or the Express hostname fallback. */
|
|
7
17
|
host: string;
|
|
18
|
+
/** Port resolved from the host header, omitted when it matches the default for the protocol. */
|
|
8
19
|
port?: number;
|
|
20
|
+
/** HTTP method (e.g. `GET`, `POST`); empty string when not provided on the underlying request. */
|
|
9
21
|
method: string;
|
|
22
|
+
/** Base URL in the form `protocol://host[:port]`, with default ports omitted. */
|
|
10
23
|
baseUrl: string;
|
|
24
|
+
/**
|
|
25
|
+
* Request path with query string (not an absolute URL), e.g. `/graphql?op=Foo`. Sourced from
|
|
26
|
+
* Express `req.originalUrl` for HTTP (preserved across router mounts/rewrites) or raw `req.url`
|
|
27
|
+
* for WebSocket upgrades. Combine with `baseUrl` to form an absolute URL.
|
|
28
|
+
*/
|
|
11
29
|
url: string;
|
|
30
|
+
/** Value of the `origin` header, or empty string when absent. */
|
|
12
31
|
origin: string;
|
|
32
|
+
/** Value of the `referer` header, when present. */
|
|
13
33
|
referer?: string;
|
|
34
|
+
/** Value of the `x-correlation-id` header for cross-service request correlation, when present. */
|
|
14
35
|
correlationId?: string;
|
|
36
|
+
/** Azure Application Request Routing log id from the `x-arr-log-id` header, when present. */
|
|
15
37
|
arrLogId?: string;
|
|
38
|
+
/** Client IP from the first `x-forwarded-for` entry, falling back to the socket remote address. */
|
|
16
39
|
clientIp?: string;
|
|
40
|
+
/** Value of the `user-agent` header, when present. */
|
|
17
41
|
userAgent?: string;
|
|
18
42
|
}
|
|
19
43
|
/**
|
package/request-info.d.ts
CHANGED
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
import type { Request } from 'express';
|
|
2
2
|
import type { IncomingMessage } from 'http';
|
|
3
|
+
/**
|
|
4
|
+
* Normalised metadata describing an inbound HTTP request or WebSocket upgrade, used as a
|
|
5
|
+
* common shape for logging, tracing, and request-scoped context across HTTP and subscription
|
|
6
|
+
* transports. Extends `Record<string, unknown>` so consumers can augment it with additional
|
|
7
|
+
* fields without losing type compatibility.
|
|
8
|
+
*/
|
|
3
9
|
export interface BaseRequestInfo extends Record<string, unknown> {
|
|
10
|
+
/** Unique identifier for the request, taken from the `x-request-id` header or generated as a UUID. */
|
|
4
11
|
requestId: string;
|
|
12
|
+
/** Transport that produced the request: `http` for Express requests, `subscription` for WebSocket upgrades. */
|
|
5
13
|
source: 'http' | 'subscription';
|
|
14
|
+
/** Resolved scheme of the request, accounting for TLS termination and `x-forwarded-proto`. */
|
|
6
15
|
protocol: 'http' | 'https' | 'ws' | 'wss';
|
|
16
|
+
/** Hostname resolved from `x-forwarded-host`, the `host` header, or the Express hostname fallback. */
|
|
7
17
|
host: string;
|
|
18
|
+
/** Port resolved from the host header, omitted when it matches the default for the protocol. */
|
|
8
19
|
port?: number;
|
|
20
|
+
/** HTTP method (e.g. `GET`, `POST`); empty string when not provided on the underlying request. */
|
|
9
21
|
method: string;
|
|
22
|
+
/** Base URL in the form `protocol://host[:port]`, with default ports omitted. */
|
|
10
23
|
baseUrl: string;
|
|
24
|
+
/**
|
|
25
|
+
* Request path with query string (not an absolute URL), e.g. `/graphql?op=Foo`. Sourced from
|
|
26
|
+
* Express `req.originalUrl` for HTTP (preserved across router mounts/rewrites) or raw `req.url`
|
|
27
|
+
* for WebSocket upgrades. Combine with `baseUrl` to form an absolute URL.
|
|
28
|
+
*/
|
|
11
29
|
url: string;
|
|
30
|
+
/** Value of the `origin` header, or empty string when absent. */
|
|
12
31
|
origin: string;
|
|
32
|
+
/** Value of the `referer` header, when present. */
|
|
13
33
|
referer?: string;
|
|
34
|
+
/** Value of the `x-correlation-id` header for cross-service request correlation, when present. */
|
|
14
35
|
correlationId?: string;
|
|
36
|
+
/** Azure Application Request Routing log id from the `x-arr-log-id` header, when present. */
|
|
15
37
|
arrLogId?: string;
|
|
38
|
+
/** Client IP from the first `x-forwarded-for` entry, falling back to the socket remote address. */
|
|
16
39
|
clientIp?: string;
|
|
40
|
+
/** Value of the `user-agent` header, when present. */
|
|
17
41
|
userAgent?: string;
|
|
18
42
|
}
|
|
19
43
|
/**
|
package/request-info.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-info.js","sources":["../src/request-info.ts"],"sourcesContent":["import { randomUUID } from 'crypto'\nimport type { Request } from 'express'\nimport type { IncomingMessage } from 'http'\nimport type { TLSSocket } from 'tls'\n\nexport interface BaseRequestInfo extends Record<string, unknown> {\n requestId: string\n source: 'http' | 'subscription'\n protocol: 'http' | 'https' | 'ws' | 'wss'\n host: string\n port?: number\n method: string\n baseUrl: string\n url: string\n origin: string\n referer?: string\n correlationId?: string\n arrLogId?: string\n clientIp?: string\n userAgent?: string\n}\n\nconst isDefaultPort = (protocol: string, port: number | undefined): boolean =>\n port == null || (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)\n\nconst formatBaseUrl = (protocol: string, hostname: string, port: number | undefined): string =>\n isDefaultPort(protocol, port) ? `${protocol}://${hostname}` : `${protocol}://${hostname}:${port}`\n\nconst firstHeaderValue = (value: string | string[] | undefined): string | undefined => {\n const raw = Array.isArray(value) ? value[0] : value\n return raw?.split(',')[0]?.trim() || undefined\n}\n\nconst parseHostHeader = (hostHeader: string): { hostname: string; port: number | undefined } => {\n const url = new URL(`http://${hostHeader}`)\n return { hostname: url.hostname, port: url.port ? Number(url.port) : undefined }\n}\n\nconst isEncryptedSocket = (req: IncomingMessage): boolean => 'encrypted' in req.socket && (req.socket as TLSSocket).encrypted === true\n\nconst isEncryptedConnect = (req: IncomingMessage): boolean => {\n const forwarded = firstHeaderValue(req.headers['x-forwarded-proto'])?.toLowerCase()\n if (forwarded === 'https' || forwarded === 'wss') return true\n if (forwarded === 'http' || forwarded === 'ws') return false\n return isEncryptedSocket(req)\n}\n\nconst resolveForwardedHost = (req: IncomingMessage): string | undefined => firstHeaderValue(req.headers['x-forwarded-host'])\n\nconst resolveHostAndPort = (req: IncomingMessage, fallbackHostname?: string): { host: string; port: number | undefined } => {\n const headerValue = resolveForwardedHost(req) ?? req.headers.host\n if (headerValue) {\n const { hostname, port } = parseHostHeader(headerValue)\n return { host: hostname, port }\n }\n return { host: fallbackHostname ?? '', port: undefined }\n}\n\n/**\n * Builds the base URL (`protocol://host[:port]`) for an Express HTTP request, using the request's\n * protocol and resolving the host/port from forwarding headers with the Express hostname as fallback.\n * The port is omitted when it matches the default for the protocol (80 for http, 443 for https).\n * @param req The Express request.\n * @returns The base URL string.\n */\nexport const requestBaseUrl = (req: Request): string => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return formatBaseUrl(req.protocol, host, port)\n}\n\n/**\n * Builds the base URL (`http(s)://host[:port]`) for a raw WebSocket upgrade (connect) request.\n * Returns an `https` URL when the connection is TLS-encrypted or `x-forwarded-proto` indicates a\n * secure scheme, otherwise `http`. Throws if no host can be determined from the request headers,\n * since upgrade requests have no Express `hostname` fallback.\n * @param req The incoming upgrade request.\n * @returns The base URL string.\n * @throws Error when the host cannot be resolved from the request.\n */\nexport const connectRequestBaseUrl = (req: IncomingMessage): string => {\n const { host, port } = resolveHostAndPort(req)\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return formatBaseUrl(isEncryptedConnect(req) ? 'https' : 'http', host, port)\n}\n\nconst buildSharedRequestInfo = (req: IncomingMessage) => ({\n requestId: req.headers['x-request-id']?.toString() ?? randomUUID(),\n method: req.method ?? '',\n origin: req.headers.origin ?? '',\n referer: req.headers.referer?.toString() ?? '',\n arrLogId: req.headers['x-arr-log-id']?.toString() ?? undefined,\n clientIp: firstHeaderValue(req.headers['x-forwarded-for']) ?? req.socket.remoteAddress,\n correlationId: req.headers['x-correlation-id']?.toString() ?? undefined,\n userAgent: req.headers['user-agent']?.toString() ?? undefined,\n})\n\nexport const buildBaseRequestInfo = (req: Request): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return {\n ...buildSharedRequestInfo(req),\n source: 'http',\n protocol: req.protocol as 'http' | 'https',\n host,\n port,\n baseUrl: formatBaseUrl(req.protocol, host, port),\n url: req.originalUrl,\n }\n}\n\nexport const buildConnectRequestInfo = (req: IncomingMessage): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req)\n const protocol = isEncryptedConnect(req) ? 'wss' : 'ws'\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return {\n ...buildSharedRequestInfo(req),\n source: 'subscription',\n protocol,\n host,\n port,\n baseUrl: formatBaseUrl(protocol === 'wss' ? 'https' : 'http', host, port),\n url: req.url ?? '',\n }\n}\n"],"names":["randomUUID"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"request-info.js","sources":["../src/request-info.ts"],"sourcesContent":["import { randomUUID } from 'crypto'\nimport type { Request } from 'express'\nimport type { IncomingMessage } from 'http'\nimport type { TLSSocket } from 'tls'\n\n/**\n * Normalised metadata describing an inbound HTTP request or WebSocket upgrade, used as a\n * common shape for logging, tracing, and request-scoped context across HTTP and subscription\n * transports. Extends `Record<string, unknown>` so consumers can augment it with additional\n * fields without losing type compatibility.\n */\nexport interface BaseRequestInfo extends Record<string, unknown> {\n /** Unique identifier for the request, taken from the `x-request-id` header or generated as a UUID. */\n requestId: string\n /** Transport that produced the request: `http` for Express requests, `subscription` for WebSocket upgrades. */\n source: 'http' | 'subscription'\n /** Resolved scheme of the request, accounting for TLS termination and `x-forwarded-proto`. */\n protocol: 'http' | 'https' | 'ws' | 'wss'\n /** Hostname resolved from `x-forwarded-host`, the `host` header, or the Express hostname fallback. */\n host: string\n /** Port resolved from the host header, omitted when it matches the default for the protocol. */\n port?: number\n /** HTTP method (e.g. `GET`, `POST`); empty string when not provided on the underlying request. */\n method: string\n /** Base URL in the form `protocol://host[:port]`, with default ports omitted. */\n baseUrl: string\n /**\n * Request path with query string (not an absolute URL), e.g. `/graphql?op=Foo`. Sourced from\n * Express `req.originalUrl` for HTTP (preserved across router mounts/rewrites) or raw `req.url`\n * for WebSocket upgrades. Combine with `baseUrl` to form an absolute URL.\n */\n url: string\n /** Value of the `origin` header, or empty string when absent. */\n origin: string\n /** Value of the `referer` header, when present. */\n referer?: string\n /** Value of the `x-correlation-id` header for cross-service request correlation, when present. */\n correlationId?: string\n /** Azure Application Request Routing log id from the `x-arr-log-id` header, when present. */\n arrLogId?: string\n /** Client IP from the first `x-forwarded-for` entry, falling back to the socket remote address. */\n clientIp?: string\n /** Value of the `user-agent` header, when present. */\n userAgent?: string\n}\n\nconst isDefaultPort = (protocol: string, port: number | undefined): boolean =>\n port == null || (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)\n\nconst formatBaseUrl = (protocol: string, hostname: string, port: number | undefined): string =>\n isDefaultPort(protocol, port) ? `${protocol}://${hostname}` : `${protocol}://${hostname}:${port}`\n\nconst firstHeaderValue = (value: string | string[] | undefined): string | undefined => {\n const raw = Array.isArray(value) ? value[0] : value\n return raw?.split(',')[0]?.trim() || undefined\n}\n\nconst parseHostHeader = (hostHeader: string): { hostname: string; port: number | undefined } => {\n const url = new URL(`http://${hostHeader}`)\n return { hostname: url.hostname, port: url.port ? Number(url.port) : undefined }\n}\n\nconst isEncryptedSocket = (req: IncomingMessage): boolean => 'encrypted' in req.socket && (req.socket as TLSSocket).encrypted === true\n\nconst isEncryptedConnect = (req: IncomingMessage): boolean => {\n const forwarded = firstHeaderValue(req.headers['x-forwarded-proto'])?.toLowerCase()\n if (forwarded === 'https' || forwarded === 'wss') return true\n if (forwarded === 'http' || forwarded === 'ws') return false\n return isEncryptedSocket(req)\n}\n\nconst resolveForwardedHost = (req: IncomingMessage): string | undefined => firstHeaderValue(req.headers['x-forwarded-host'])\n\nconst resolveHostAndPort = (req: IncomingMessage, fallbackHostname?: string): { host: string; port: number | undefined } => {\n const headerValue = resolveForwardedHost(req) ?? req.headers.host\n if (headerValue) {\n const { hostname, port } = parseHostHeader(headerValue)\n return { host: hostname, port }\n }\n return { host: fallbackHostname ?? '', port: undefined }\n}\n\n/**\n * Builds the base URL (`protocol://host[:port]`) for an Express HTTP request, using the request's\n * protocol and resolving the host/port from forwarding headers with the Express hostname as fallback.\n * The port is omitted when it matches the default for the protocol (80 for http, 443 for https).\n * @param req The Express request.\n * @returns The base URL string.\n */\nexport const requestBaseUrl = (req: Request): string => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return formatBaseUrl(req.protocol, host, port)\n}\n\n/**\n * Builds the base URL (`http(s)://host[:port]`) for a raw WebSocket upgrade (connect) request.\n * Returns an `https` URL when the connection is TLS-encrypted or `x-forwarded-proto` indicates a\n * secure scheme, otherwise `http`. Throws if no host can be determined from the request headers,\n * since upgrade requests have no Express `hostname` fallback.\n * @param req The incoming upgrade request.\n * @returns The base URL string.\n * @throws Error when the host cannot be resolved from the request.\n */\nexport const connectRequestBaseUrl = (req: IncomingMessage): string => {\n const { host, port } = resolveHostAndPort(req)\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return formatBaseUrl(isEncryptedConnect(req) ? 'https' : 'http', host, port)\n}\n\nconst buildSharedRequestInfo = (req: IncomingMessage) => ({\n requestId: req.headers['x-request-id']?.toString() ?? randomUUID(),\n method: req.method ?? '',\n origin: req.headers.origin ?? '',\n referer: req.headers.referer?.toString() ?? '',\n arrLogId: req.headers['x-arr-log-id']?.toString() ?? undefined,\n clientIp: firstHeaderValue(req.headers['x-forwarded-for']) ?? req.socket.remoteAddress,\n correlationId: req.headers['x-correlation-id']?.toString() ?? undefined,\n userAgent: req.headers['user-agent']?.toString() ?? undefined,\n})\n\nexport const buildBaseRequestInfo = (req: Request): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return {\n ...buildSharedRequestInfo(req),\n source: 'http',\n protocol: req.protocol as 'http' | 'https',\n host,\n port,\n baseUrl: formatBaseUrl(req.protocol, host, port),\n url: req.originalUrl,\n }\n}\n\nexport const buildConnectRequestInfo = (req: IncomingMessage): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req)\n const protocol = isEncryptedConnect(req) ? 'wss' : 'ws'\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return {\n ...buildSharedRequestInfo(req),\n source: 'subscription',\n protocol,\n host,\n port,\n baseUrl: formatBaseUrl(protocol === 'wss' ? 'https' : 'http', host, port),\n url: req.url ?? '',\n }\n}\n"],"names":["randomUUID"],"mappings":"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"}
|
package/request-info.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-info.mjs","sources":["../src/request-info.ts"],"sourcesContent":["import { randomUUID } from 'crypto'\nimport type { Request } from 'express'\nimport type { IncomingMessage } from 'http'\nimport type { TLSSocket } from 'tls'\n\nexport interface BaseRequestInfo extends Record<string, unknown> {\n requestId: string\n source: 'http' | 'subscription'\n protocol: 'http' | 'https' | 'ws' | 'wss'\n host: string\n port?: number\n method: string\n baseUrl: string\n url: string\n origin: string\n referer?: string\n correlationId?: string\n arrLogId?: string\n clientIp?: string\n userAgent?: string\n}\n\nconst isDefaultPort = (protocol: string, port: number | undefined): boolean =>\n port == null || (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)\n\nconst formatBaseUrl = (protocol: string, hostname: string, port: number | undefined): string =>\n isDefaultPort(protocol, port) ? `${protocol}://${hostname}` : `${protocol}://${hostname}:${port}`\n\nconst firstHeaderValue = (value: string | string[] | undefined): string | undefined => {\n const raw = Array.isArray(value) ? value[0] : value\n return raw?.split(',')[0]?.trim() || undefined\n}\n\nconst parseHostHeader = (hostHeader: string): { hostname: string; port: number | undefined } => {\n const url = new URL(`http://${hostHeader}`)\n return { hostname: url.hostname, port: url.port ? Number(url.port) : undefined }\n}\n\nconst isEncryptedSocket = (req: IncomingMessage): boolean => 'encrypted' in req.socket && (req.socket as TLSSocket).encrypted === true\n\nconst isEncryptedConnect = (req: IncomingMessage): boolean => {\n const forwarded = firstHeaderValue(req.headers['x-forwarded-proto'])?.toLowerCase()\n if (forwarded === 'https' || forwarded === 'wss') return true\n if (forwarded === 'http' || forwarded === 'ws') return false\n return isEncryptedSocket(req)\n}\n\nconst resolveForwardedHost = (req: IncomingMessage): string | undefined => firstHeaderValue(req.headers['x-forwarded-host'])\n\nconst resolveHostAndPort = (req: IncomingMessage, fallbackHostname?: string): { host: string; port: number | undefined } => {\n const headerValue = resolveForwardedHost(req) ?? req.headers.host\n if (headerValue) {\n const { hostname, port } = parseHostHeader(headerValue)\n return { host: hostname, port }\n }\n return { host: fallbackHostname ?? '', port: undefined }\n}\n\n/**\n * Builds the base URL (`protocol://host[:port]`) for an Express HTTP request, using the request's\n * protocol and resolving the host/port from forwarding headers with the Express hostname as fallback.\n * The port is omitted when it matches the default for the protocol (80 for http, 443 for https).\n * @param req The Express request.\n * @returns The base URL string.\n */\nexport const requestBaseUrl = (req: Request): string => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return formatBaseUrl(req.protocol, host, port)\n}\n\n/**\n * Builds the base URL (`http(s)://host[:port]`) for a raw WebSocket upgrade (connect) request.\n * Returns an `https` URL when the connection is TLS-encrypted or `x-forwarded-proto` indicates a\n * secure scheme, otherwise `http`. Throws if no host can be determined from the request headers,\n * since upgrade requests have no Express `hostname` fallback.\n * @param req The incoming upgrade request.\n * @returns The base URL string.\n * @throws Error when the host cannot be resolved from the request.\n */\nexport const connectRequestBaseUrl = (req: IncomingMessage): string => {\n const { host, port } = resolveHostAndPort(req)\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return formatBaseUrl(isEncryptedConnect(req) ? 'https' : 'http', host, port)\n}\n\nconst buildSharedRequestInfo = (req: IncomingMessage) => ({\n requestId: req.headers['x-request-id']?.toString() ?? randomUUID(),\n method: req.method ?? '',\n origin: req.headers.origin ?? '',\n referer: req.headers.referer?.toString() ?? '',\n arrLogId: req.headers['x-arr-log-id']?.toString() ?? undefined,\n clientIp: firstHeaderValue(req.headers['x-forwarded-for']) ?? req.socket.remoteAddress,\n correlationId: req.headers['x-correlation-id']?.toString() ?? undefined,\n userAgent: req.headers['user-agent']?.toString() ?? undefined,\n})\n\nexport const buildBaseRequestInfo = (req: Request): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return {\n ...buildSharedRequestInfo(req),\n source: 'http',\n protocol: req.protocol as 'http' | 'https',\n host,\n port,\n baseUrl: formatBaseUrl(req.protocol, host, port),\n url: req.originalUrl,\n }\n}\n\nexport const buildConnectRequestInfo = (req: IncomingMessage): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req)\n const protocol = isEncryptedConnect(req) ? 'wss' : 'ws'\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return {\n ...buildSharedRequestInfo(req),\n source: 'subscription',\n protocol,\n host,\n port,\n baseUrl: formatBaseUrl(protocol === 'wss' ? 'https' : 'http', host, port),\n url: req.url ?? '',\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"request-info.mjs","sources":["../src/request-info.ts"],"sourcesContent":["import { randomUUID } from 'crypto'\nimport type { Request } from 'express'\nimport type { IncomingMessage } from 'http'\nimport type { TLSSocket } from 'tls'\n\n/**\n * Normalised metadata describing an inbound HTTP request or WebSocket upgrade, used as a\n * common shape for logging, tracing, and request-scoped context across HTTP and subscription\n * transports. Extends `Record<string, unknown>` so consumers can augment it with additional\n * fields without losing type compatibility.\n */\nexport interface BaseRequestInfo extends Record<string, unknown> {\n /** Unique identifier for the request, taken from the `x-request-id` header or generated as a UUID. */\n requestId: string\n /** Transport that produced the request: `http` for Express requests, `subscription` for WebSocket upgrades. */\n source: 'http' | 'subscription'\n /** Resolved scheme of the request, accounting for TLS termination and `x-forwarded-proto`. */\n protocol: 'http' | 'https' | 'ws' | 'wss'\n /** Hostname resolved from `x-forwarded-host`, the `host` header, or the Express hostname fallback. */\n host: string\n /** Port resolved from the host header, omitted when it matches the default for the protocol. */\n port?: number\n /** HTTP method (e.g. `GET`, `POST`); empty string when not provided on the underlying request. */\n method: string\n /** Base URL in the form `protocol://host[:port]`, with default ports omitted. */\n baseUrl: string\n /**\n * Request path with query string (not an absolute URL), e.g. `/graphql?op=Foo`. Sourced from\n * Express `req.originalUrl` for HTTP (preserved across router mounts/rewrites) or raw `req.url`\n * for WebSocket upgrades. Combine with `baseUrl` to form an absolute URL.\n */\n url: string\n /** Value of the `origin` header, or empty string when absent. */\n origin: string\n /** Value of the `referer` header, when present. */\n referer?: string\n /** Value of the `x-correlation-id` header for cross-service request correlation, when present. */\n correlationId?: string\n /** Azure Application Request Routing log id from the `x-arr-log-id` header, when present. */\n arrLogId?: string\n /** Client IP from the first `x-forwarded-for` entry, falling back to the socket remote address. */\n clientIp?: string\n /** Value of the `user-agent` header, when present. */\n userAgent?: string\n}\n\nconst isDefaultPort = (protocol: string, port: number | undefined): boolean =>\n port == null || (protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)\n\nconst formatBaseUrl = (protocol: string, hostname: string, port: number | undefined): string =>\n isDefaultPort(protocol, port) ? `${protocol}://${hostname}` : `${protocol}://${hostname}:${port}`\n\nconst firstHeaderValue = (value: string | string[] | undefined): string | undefined => {\n const raw = Array.isArray(value) ? value[0] : value\n return raw?.split(',')[0]?.trim() || undefined\n}\n\nconst parseHostHeader = (hostHeader: string): { hostname: string; port: number | undefined } => {\n const url = new URL(`http://${hostHeader}`)\n return { hostname: url.hostname, port: url.port ? Number(url.port) : undefined }\n}\n\nconst isEncryptedSocket = (req: IncomingMessage): boolean => 'encrypted' in req.socket && (req.socket as TLSSocket).encrypted === true\n\nconst isEncryptedConnect = (req: IncomingMessage): boolean => {\n const forwarded = firstHeaderValue(req.headers['x-forwarded-proto'])?.toLowerCase()\n if (forwarded === 'https' || forwarded === 'wss') return true\n if (forwarded === 'http' || forwarded === 'ws') return false\n return isEncryptedSocket(req)\n}\n\nconst resolveForwardedHost = (req: IncomingMessage): string | undefined => firstHeaderValue(req.headers['x-forwarded-host'])\n\nconst resolveHostAndPort = (req: IncomingMessage, fallbackHostname?: string): { host: string; port: number | undefined } => {\n const headerValue = resolveForwardedHost(req) ?? req.headers.host\n if (headerValue) {\n const { hostname, port } = parseHostHeader(headerValue)\n return { host: hostname, port }\n }\n return { host: fallbackHostname ?? '', port: undefined }\n}\n\n/**\n * Builds the base URL (`protocol://host[:port]`) for an Express HTTP request, using the request's\n * protocol and resolving the host/port from forwarding headers with the Express hostname as fallback.\n * The port is omitted when it matches the default for the protocol (80 for http, 443 for https).\n * @param req The Express request.\n * @returns The base URL string.\n */\nexport const requestBaseUrl = (req: Request): string => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return formatBaseUrl(req.protocol, host, port)\n}\n\n/**\n * Builds the base URL (`http(s)://host[:port]`) for a raw WebSocket upgrade (connect) request.\n * Returns an `https` URL when the connection is TLS-encrypted or `x-forwarded-proto` indicates a\n * secure scheme, otherwise `http`. Throws if no host can be determined from the request headers,\n * since upgrade requests have no Express `hostname` fallback.\n * @param req The incoming upgrade request.\n * @returns The base URL string.\n * @throws Error when the host cannot be resolved from the request.\n */\nexport const connectRequestBaseUrl = (req: IncomingMessage): string => {\n const { host, port } = resolveHostAndPort(req)\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return formatBaseUrl(isEncryptedConnect(req) ? 'https' : 'http', host, port)\n}\n\nconst buildSharedRequestInfo = (req: IncomingMessage) => ({\n requestId: req.headers['x-request-id']?.toString() ?? randomUUID(),\n method: req.method ?? '',\n origin: req.headers.origin ?? '',\n referer: req.headers.referer?.toString() ?? '',\n arrLogId: req.headers['x-arr-log-id']?.toString() ?? undefined,\n clientIp: firstHeaderValue(req.headers['x-forwarded-for']) ?? req.socket.remoteAddress,\n correlationId: req.headers['x-correlation-id']?.toString() ?? undefined,\n userAgent: req.headers['user-agent']?.toString() ?? undefined,\n})\n\nexport const buildBaseRequestInfo = (req: Request): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req, req.hostname)\n return {\n ...buildSharedRequestInfo(req),\n source: 'http',\n protocol: req.protocol as 'http' | 'https',\n host,\n port,\n baseUrl: formatBaseUrl(req.protocol, host, port),\n url: req.originalUrl,\n }\n}\n\nexport const buildConnectRequestInfo = (req: IncomingMessage): BaseRequestInfo => {\n const { host, port } = resolveHostAndPort(req)\n const protocol = isEncryptedConnect(req) ? 'wss' : 'ws'\n if (!host) throw new Error('Cannot determine base URL from websocket connect request')\n return {\n ...buildSharedRequestInfo(req),\n source: 'subscription',\n protocol,\n host,\n port,\n baseUrl: formatBaseUrl(protocol === 'wss' ? 'https' : 'http', host, port),\n url: req.url ?? '',\n }\n}\n"],"names":[],"mappings":"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"}
|
package/shield.d.cts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
+
import type { GraphQLResolveInfo } from 'graphql';
|
|
1
2
|
import type { allow, and, chain, or, race } from 'graphql-shield';
|
|
2
|
-
import { shield } from 'graphql-shield';
|
|
3
|
+
import { rule, shield } from 'graphql-shield';
|
|
3
4
|
import type { Primitive } from './type-utils';
|
|
4
5
|
export type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>;
|
|
5
6
|
type RuleCombinator = typeof chain | typeof race | typeof or | typeof and;
|
|
6
7
|
export type ShieldRule = ReturnType<(typeof allow)['getRules']>[number];
|
|
8
|
+
type RuleConstructorOptions = NonNullable<Parameters<typeof rule>[1]>;
|
|
9
|
+
type RuleResult = boolean | string | Error;
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
11
|
+
* Strongly typed wrapper around graphql-shield's `rule(...)`.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
15
|
-
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
16
|
-
*
|
|
17
|
-
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
18
|
-
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
13
|
+
* Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the
|
|
14
|
+
* same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on
|
|
15
|
+
* `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the
|
|
16
|
+
* underlying `rule` produces.
|
|
19
17
|
*
|
|
20
18
|
* Usage:
|
|
21
19
|
*
|
|
22
|
-
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
20
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>()(
|
|
23
21
|
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
24
22
|
* )
|
|
23
|
+
*
|
|
24
|
+
* const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(
|
|
25
|
+
* (_, __, ctx) => ctx.user?.roles.includes('admin') === true,
|
|
26
|
+
* )
|
|
25
27
|
*/
|
|
26
|
-
export declare const createRule: <TContext, TParent = unknown, TArgs = unknown>(
|
|
28
|
+
export declare const createRule: <TContext, TParent = unknown, TArgs = unknown>(name?: string | RuleConstructorOptions, options?: RuleConstructorOptions) => (fn: (parent: TParent, args: TArgs, ctx: TContext, info: GraphQLResolveInfo) => RuleResult | Promise<RuleResult>) => ShieldRule;
|
|
27
29
|
/**
|
|
28
30
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
29
31
|
* @param schema The existing shield schema with rules
|
package/shield.d.mts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
+
import type { GraphQLResolveInfo } from 'graphql';
|
|
1
2
|
import type { allow, and, chain, or, race } from 'graphql-shield';
|
|
2
|
-
import { shield } from 'graphql-shield';
|
|
3
|
+
import { rule, shield } from 'graphql-shield';
|
|
3
4
|
import type { Primitive } from './type-utils.js';
|
|
4
5
|
export type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>;
|
|
5
6
|
type RuleCombinator = typeof chain | typeof race | typeof or | typeof and;
|
|
6
7
|
export type ShieldRule = ReturnType<(typeof allow)['getRules']>[number];
|
|
8
|
+
type RuleConstructorOptions = NonNullable<Parameters<typeof rule>[1]>;
|
|
9
|
+
type RuleResult = boolean | string | Error;
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
11
|
+
* Strongly typed wrapper around graphql-shield's `rule(...)`.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
15
|
-
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
16
|
-
*
|
|
17
|
-
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
18
|
-
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
13
|
+
* Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the
|
|
14
|
+
* same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on
|
|
15
|
+
* `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the
|
|
16
|
+
* underlying `rule` produces.
|
|
19
17
|
*
|
|
20
18
|
* Usage:
|
|
21
19
|
*
|
|
22
|
-
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
20
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>()(
|
|
23
21
|
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
24
22
|
* )
|
|
23
|
+
*
|
|
24
|
+
* const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(
|
|
25
|
+
* (_, __, ctx) => ctx.user?.roles.includes('admin') === true,
|
|
26
|
+
* )
|
|
25
27
|
*/
|
|
26
|
-
export declare const createRule: <TContext, TParent = unknown, TArgs = unknown>(
|
|
28
|
+
export declare const createRule: <TContext, TParent = unknown, TArgs = unknown>(name?: string | RuleConstructorOptions, options?: RuleConstructorOptions) => (fn: (parent: TParent, args: TArgs, ctx: TContext, info: GraphQLResolveInfo) => RuleResult | Promise<RuleResult>) => ShieldRule;
|
|
27
29
|
/**
|
|
28
30
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
29
31
|
* @param schema The existing shield schema with rules
|
package/shield.d.ts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
|
+
import type { GraphQLResolveInfo } from 'graphql';
|
|
1
2
|
import type { allow, and, chain, or, race } from 'graphql-shield';
|
|
2
|
-
import { shield } from 'graphql-shield';
|
|
3
|
+
import { rule, shield } from 'graphql-shield';
|
|
3
4
|
import type { Primitive } from './type-utils';
|
|
4
5
|
export type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>;
|
|
5
6
|
type RuleCombinator = typeof chain | typeof race | typeof or | typeof and;
|
|
6
7
|
export type ShieldRule = ReturnType<(typeof allow)['getRules']>[number];
|
|
8
|
+
type RuleConstructorOptions = NonNullable<Parameters<typeof rule>[1]>;
|
|
9
|
+
type RuleResult = boolean | string | Error;
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
11
|
+
* Strongly typed wrapper around graphql-shield's `rule(...)`.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
15
|
-
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
16
|
-
*
|
|
17
|
-
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
18
|
-
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
13
|
+
* Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the
|
|
14
|
+
* same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on
|
|
15
|
+
* `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the
|
|
16
|
+
* underlying `rule` produces.
|
|
19
17
|
*
|
|
20
18
|
* Usage:
|
|
21
19
|
*
|
|
22
|
-
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
20
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>()(
|
|
23
21
|
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
24
22
|
* )
|
|
23
|
+
*
|
|
24
|
+
* const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(
|
|
25
|
+
* (_, __, ctx) => ctx.user?.roles.includes('admin') === true,
|
|
26
|
+
* )
|
|
25
27
|
*/
|
|
26
|
-
export declare const createRule: <TContext, TParent = unknown, TArgs = unknown>(
|
|
28
|
+
export declare const createRule: <TContext, TParent = unknown, TArgs = unknown>(name?: string | RuleConstructorOptions, options?: RuleConstructorOptions) => (fn: (parent: TParent, args: TArgs, ctx: TContext, info: GraphQLResolveInfo) => RuleResult | Promise<RuleResult>) => ShieldRule;
|
|
27
29
|
/**
|
|
28
30
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
29
31
|
* @param schema The existing shield schema with rules
|
package/shield.js
CHANGED
|
@@ -3,25 +3,24 @@
|
|
|
3
3
|
var graphqlShield = require('graphql-shield');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Strongly typed wrapper around graphql-shield's `rule(...)`.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
13
|
-
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
14
|
-
*
|
|
15
|
-
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
16
|
-
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
8
|
+
* Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the
|
|
9
|
+
* same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on
|
|
10
|
+
* `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the
|
|
11
|
+
* underlying `rule` produces.
|
|
17
12
|
*
|
|
18
13
|
* Usage:
|
|
19
14
|
*
|
|
20
|
-
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
15
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>()(
|
|
21
16
|
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
22
17
|
* )
|
|
18
|
+
*
|
|
19
|
+
* const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(
|
|
20
|
+
* (_, __, ctx) => ctx.user?.roles.includes('admin') === true,
|
|
21
|
+
* )
|
|
23
22
|
*/
|
|
24
|
-
const createRule = (
|
|
23
|
+
const createRule = (name, options) => (fn) => graphqlShield.rule(name, options)(fn);
|
|
25
24
|
/**
|
|
26
25
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
27
26
|
* @param schema The existing shield schema with rules
|
package/shield.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shield.js","sources":["../src/shield.ts"],"sourcesContent":["import type { allow, and, chain, IRules, or, race } from 'graphql-shield'\nimport { rule, shield } from 'graphql-shield'\nimport type { GraphQLContext } from './context'\nimport type { Primitive } from './type-utils'\n\nexport type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>\n\ntype RuleCombinator = typeof chain | typeof race | typeof or | typeof and\n// For whatever reason, graphql-shield doesn't export this type, but we can extract if from\n// what it does export.\nexport type ShieldRule = ReturnType<(typeof allow)['getRules']>[number]\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"shield.js","sources":["../src/shield.ts"],"sourcesContent":["import type { GraphQLResolveInfo } from 'graphql'\nimport type { allow, and, chain, IRules, or, race } from 'graphql-shield'\nimport { rule, shield } from 'graphql-shield'\nimport type { GraphQLContext } from './context'\nimport type { Primitive } from './type-utils'\n\nexport type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>\n\ntype RuleCombinator = typeof chain | typeof race | typeof or | typeof and\n// For whatever reason, graphql-shield doesn't export this type, but we can extract if from\n// what it does export.\nexport type ShieldRule = ReturnType<(typeof allow)['getRules']>[number]\n\ntype RuleConstructorOptions = NonNullable<Parameters<typeof rule>[1]>\ntype RuleResult = boolean | string | Error\ntype ShieldRuleFn = Parameters<ReturnType<typeof rule>>[0]\n\n/**\n * Strongly typed wrapper around graphql-shield's `rule(...)`.\n *\n * Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the\n * same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on\n * `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the\n * underlying `rule` produces.\n *\n * Usage:\n *\n * const isOwner = createRule<GraphQLContext, { ownerId: string }>()(\n * (parent, _, ctx) => parent.ownerId === ctx.user?.id,\n * )\n *\n * const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(\n * (_, __, ctx) => ctx.user?.roles.includes('admin') === true,\n * )\n */\nexport const createRule =\n <TContext, TParent = unknown, TArgs = unknown>(name?: string | RuleConstructorOptions, options?: RuleConstructorOptions) =>\n (fn: (parent: TParent, args: TArgs, ctx: TContext, info: GraphQLResolveInfo) => RuleResult | Promise<RuleResult>): ShieldRule =>\n rule(name, options)(fn as ShieldRuleFn)\n\n/**\n * Combines a given rule with all existing defined rules using the provided combinator\n * @param schema The existing shield schema with rules\n * @param rule The rule to be combined with existing rules\n * @param combinator The combinator to use. Eg. and | or | chain | race\n *\n * Usage:\n *\n * const updatedSchema = combineRuleWithAll(shieldSchema, rules.accountNotBlocked, chain)\n */\nexport function combineRuleWithAll<T>(schema: ShieldSchema<T>, rule: ShieldRule, combinator: RuleCombinator): ShieldSchema<T> {\n return Object.fromEntries(\n Object.entries(schema).map(([key, value]) => {\n if (value.constructor.name !== 'Object') {\n return [key, combinator(rule, value)]\n }\n return [key, combineRuleWithAll(value, rule, combinator)]\n }),\n ) as ShieldSchema<T>\n}\n\n/**\n * Creates graphql shield middleware making use of generics to type the definition of the rules.\n * @param schema A mapping of schema objects to the shield rules that define the access to that object\n * @param options Shield options forwarded to `shield`. Defaults `allowExternalErrors` to `true`.\n *\n * Usage:\n * import { Resolvers } from '../generated/types'\n *\n * const isCoordinator = hasRoleRule(UserRoles.Coordinator)\n * const isSystemAdmin = hasRoleRule(UserRoles.SystemAdmin)\n * const isQualityAssurance = hasRoleRule(UserRoles.QualityAssurance)\n *\n * const isAuthorisedUser = or(isCoordinator, isSystemAdmin, isQualityAssurance)\n * const isDataAuthor = or(isCoordinator, isSystemAdmin)\n *\n * const shieldSchema = createShieldSchema<Resolvers>({\n * Query: {\n * '*': isAuthorisedUser,\n * user: isCoordinator,\n * findUsers: isCoordinator,\n * },\n * Mutation: {\n * '*': isDataAuthor,\n * createUser: isCoordinator,\n * },\n * // specific rules for mutations, types, fields...\n * }, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError()})\n *\n * const unprotected = makeExecutableSchema({...})\n * return applyMiddleware(unprotected, shieldSchema)\n */\nexport function createShieldSchema<TRootResolvers>(schema: ShieldSchema<TRootResolvers>, options?: ShieldOptions) {\n return shield(schema as IRules, {\n allowExternalErrors: true,\n ...options,\n })\n}\n\n/**\n * A shield rule tree typed against a resolver map (typically your generated `Resolvers` type).\n *\n * Mirrors the shape of `TResolver` so each type/field can be assigned a {@link ShieldRule}.\n * Object levels may also include a `'*'` key — a fallback rule applied to any field at that\n * level that doesn't have an explicit rule. Leaf (primitive) positions collapse to a single\n * {@link ShieldRule}.\n *\n * Usage:\n *\n * const schema: ShieldSchema<Resolvers> = {\n * Query: {\n * '*': isAuthorisedUser,\n * publicThing: allow,\n * },\n * Mutation: { createUser: isCoordinator },\n * }\n */\nexport type ShieldSchema<TResolver> = TResolver extends Primitive\n ? ShieldRule\n : {\n [key in keyof TResolver]?: ShieldSchema<TResolver[key]> | ShieldRule\n } & {\n '*'?: ShieldRule\n }\n\n/**\n * Shield rule that passes when the current user has the given role.\n * @param role The role the user must have on `ctx.user.roles`.\n * @param ruleName Optional override for the rule's cache key. Defaults to `hasRole-${role}`.\n */\nexport function hasRoleRule(role: string, ruleName?: string): ShieldRule {\n return rule(ruleName ?? `hasRole-${role}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.roles.includes(role) === true,\n )\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles.\n * Rule name is derived from the roles; use {@link hasAnyRoleRuleWithName} to supply your own.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRule(...roles: string[]): ShieldRule {\n return hasAnyRoleRuleWithName(`hasAnyRole-${roles.join(',')}`, ...roles)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles, with an explicit rule name.\n * Prefer this over {@link hasAnyRoleRule} when the role list is large or dynamic and you want a stable cache key.\n * @param ruleName The name (and cache key) for the rule.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRuleWithName(ruleName: string, ...roles: string[]): ShieldRule {\n return rule(ruleName, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => {\n return user?.roles.some((role: string) => roles.includes(role)) === true\n })\n}\n\n/**\n * Shield rule that passes when the current user has the given scope.\n * @param scope The scope the user must have on `ctx.user.scopes`.\n */\nexport function hasScopeRule(scope: string): ShieldRule {\n return rule(`hasScope-${scope}`, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => user?.scopes.includes(scope) === true)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given scopes.\n * @param scopes The set of scopes, any of which satisfies the rule.\n */\nexport function hasAnyScopeRule(...scopes: string[]): ShieldRule {\n return rule(`hasAnyScope-${scopes.join(',')}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.scopes.some((scope: string) => scopes.includes(scope)) === true,\n )\n}\n"],"names":["rule","shield"],"mappings":"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"}
|
package/shield.mjs
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { rule, shield } from 'graphql-shield';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Strongly typed wrapper around graphql-shield's `rule(...)`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
|
|
11
|
-
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
|
|
12
|
-
*
|
|
13
|
-
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
|
|
14
|
-
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
|
|
6
|
+
* Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the
|
|
7
|
+
* same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on
|
|
8
|
+
* `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the
|
|
9
|
+
* underlying `rule` produces.
|
|
15
10
|
*
|
|
16
11
|
* Usage:
|
|
17
12
|
*
|
|
18
|
-
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
|
|
13
|
+
* const isOwner = createRule<GraphQLContext, { ownerId: string }>()(
|
|
19
14
|
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
|
|
20
15
|
* )
|
|
16
|
+
*
|
|
17
|
+
* const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(
|
|
18
|
+
* (_, __, ctx) => ctx.user?.roles.includes('admin') === true,
|
|
19
|
+
* )
|
|
21
20
|
*/
|
|
22
|
-
const createRule = (
|
|
21
|
+
const createRule = (name, options) => (fn) => rule(name, options)(fn);
|
|
23
22
|
/**
|
|
24
23
|
* Combines a given rule with all existing defined rules using the provided combinator
|
|
25
24
|
* @param schema The existing shield schema with rules
|
package/shield.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shield.mjs","sources":["../src/shield.ts"],"sourcesContent":["import type { allow, and, chain, IRules, or, race } from 'graphql-shield'\nimport { rule, shield } from 'graphql-shield'\nimport type { GraphQLContext } from './context'\nimport type { Primitive } from './type-utils'\n\nexport type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>\n\ntype RuleCombinator = typeof chain | typeof race | typeof or | typeof and\n// For whatever reason, graphql-shield doesn't export this type, but we can extract if from\n// what it does export.\nexport type ShieldRule = ReturnType<(typeof allow)['getRules']>[number]\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"shield.mjs","sources":["../src/shield.ts"],"sourcesContent":["import type { GraphQLResolveInfo } from 'graphql'\nimport type { allow, and, chain, IRules, or, race } from 'graphql-shield'\nimport { rule, shield } from 'graphql-shield'\nimport type { GraphQLContext } from './context'\nimport type { Primitive } from './type-utils'\n\nexport type ShieldOptions = NonNullable<Parameters<typeof shield>[1]>\n\ntype RuleCombinator = typeof chain | typeof race | typeof or | typeof and\n// For whatever reason, graphql-shield doesn't export this type, but we can extract if from\n// what it does export.\nexport type ShieldRule = ReturnType<(typeof allow)['getRules']>[number]\n\ntype RuleConstructorOptions = NonNullable<Parameters<typeof rule>[1]>\ntype RuleResult = boolean | string | Error\ntype ShieldRuleFn = Parameters<ReturnType<typeof rule>>[0]\n\n/**\n * Strongly typed wrapper around graphql-shield's `rule(...)`.\n *\n * Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the\n * same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on\n * `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the\n * underlying `rule` produces.\n *\n * Usage:\n *\n * const isOwner = createRule<GraphQLContext, { ownerId: string }>()(\n * (parent, _, ctx) => parent.ownerId === ctx.user?.id,\n * )\n *\n * const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(\n * (_, __, ctx) => ctx.user?.roles.includes('admin') === true,\n * )\n */\nexport const createRule =\n <TContext, TParent = unknown, TArgs = unknown>(name?: string | RuleConstructorOptions, options?: RuleConstructorOptions) =>\n (fn: (parent: TParent, args: TArgs, ctx: TContext, info: GraphQLResolveInfo) => RuleResult | Promise<RuleResult>): ShieldRule =>\n rule(name, options)(fn as ShieldRuleFn)\n\n/**\n * Combines a given rule with all existing defined rules using the provided combinator\n * @param schema The existing shield schema with rules\n * @param rule The rule to be combined with existing rules\n * @param combinator The combinator to use. Eg. and | or | chain | race\n *\n * Usage:\n *\n * const updatedSchema = combineRuleWithAll(shieldSchema, rules.accountNotBlocked, chain)\n */\nexport function combineRuleWithAll<T>(schema: ShieldSchema<T>, rule: ShieldRule, combinator: RuleCombinator): ShieldSchema<T> {\n return Object.fromEntries(\n Object.entries(schema).map(([key, value]) => {\n if (value.constructor.name !== 'Object') {\n return [key, combinator(rule, value)]\n }\n return [key, combineRuleWithAll(value, rule, combinator)]\n }),\n ) as ShieldSchema<T>\n}\n\n/**\n * Creates graphql shield middleware making use of generics to type the definition of the rules.\n * @param schema A mapping of schema objects to the shield rules that define the access to that object\n * @param options Shield options forwarded to `shield`. Defaults `allowExternalErrors` to `true`.\n *\n * Usage:\n * import { Resolvers } from '../generated/types'\n *\n * const isCoordinator = hasRoleRule(UserRoles.Coordinator)\n * const isSystemAdmin = hasRoleRule(UserRoles.SystemAdmin)\n * const isQualityAssurance = hasRoleRule(UserRoles.QualityAssurance)\n *\n * const isAuthorisedUser = or(isCoordinator, isSystemAdmin, isQualityAssurance)\n * const isDataAuthor = or(isCoordinator, isSystemAdmin)\n *\n * const shieldSchema = createShieldSchema<Resolvers>({\n * Query: {\n * '*': isAuthorisedUser,\n * user: isCoordinator,\n * findUsers: isCoordinator,\n * },\n * Mutation: {\n * '*': isDataAuthor,\n * createUser: isCoordinator,\n * },\n * // specific rules for mutations, types, fields...\n * }, { fallbackRule: isAuthorisedUser, fallbackError: unauthorisedError()})\n *\n * const unprotected = makeExecutableSchema({...})\n * return applyMiddleware(unprotected, shieldSchema)\n */\nexport function createShieldSchema<TRootResolvers>(schema: ShieldSchema<TRootResolvers>, options?: ShieldOptions) {\n return shield(schema as IRules, {\n allowExternalErrors: true,\n ...options,\n })\n}\n\n/**\n * A shield rule tree typed against a resolver map (typically your generated `Resolvers` type).\n *\n * Mirrors the shape of `TResolver` so each type/field can be assigned a {@link ShieldRule}.\n * Object levels may also include a `'*'` key — a fallback rule applied to any field at that\n * level that doesn't have an explicit rule. Leaf (primitive) positions collapse to a single\n * {@link ShieldRule}.\n *\n * Usage:\n *\n * const schema: ShieldSchema<Resolvers> = {\n * Query: {\n * '*': isAuthorisedUser,\n * publicThing: allow,\n * },\n * Mutation: { createUser: isCoordinator },\n * }\n */\nexport type ShieldSchema<TResolver> = TResolver extends Primitive\n ? ShieldRule\n : {\n [key in keyof TResolver]?: ShieldSchema<TResolver[key]> | ShieldRule\n } & {\n '*'?: ShieldRule\n }\n\n/**\n * Shield rule that passes when the current user has the given role.\n * @param role The role the user must have on `ctx.user.roles`.\n * @param ruleName Optional override for the rule's cache key. Defaults to `hasRole-${role}`.\n */\nexport function hasRoleRule(role: string, ruleName?: string): ShieldRule {\n return rule(ruleName ?? `hasRole-${role}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.roles.includes(role) === true,\n )\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles.\n * Rule name is derived from the roles; use {@link hasAnyRoleRuleWithName} to supply your own.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRule(...roles: string[]): ShieldRule {\n return hasAnyRoleRuleWithName(`hasAnyRole-${roles.join(',')}`, ...roles)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given roles, with an explicit rule name.\n * Prefer this over {@link hasAnyRoleRule} when the role list is large or dynamic and you want a stable cache key.\n * @param ruleName The name (and cache key) for the rule.\n * @param roles The set of roles, any of which satisfies the rule.\n */\nexport function hasAnyRoleRuleWithName(ruleName: string, ...roles: string[]): ShieldRule {\n return rule(ruleName, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => {\n return user?.roles.some((role: string) => roles.includes(role)) === true\n })\n}\n\n/**\n * Shield rule that passes when the current user has the given scope.\n * @param scope The scope the user must have on `ctx.user.scopes`.\n */\nexport function hasScopeRule(scope: string): ShieldRule {\n return rule(`hasScope-${scope}`, { cache: 'contextual' })((_, __, { user }: GraphQLContext) => user?.scopes.includes(scope) === true)\n}\n\n/**\n * Shield rule that passes when the current user has at least one of the given scopes.\n * @param scopes The set of scopes, any of which satisfies the rule.\n */\nexport function hasAnyScopeRule(...scopes: string[]): ShieldRule {\n return rule(`hasAnyScope-${scopes.join(',')}`, { cache: 'contextual' })(\n (_, __, { user }: GraphQLContext) => user?.scopes.some((scope: string) => scopes.includes(scope)) === true,\n )\n}\n"],"names":[],"mappings":"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"}
|
package/subscriptions/server.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var graphqlWs = require('graphql-ws');
|
|
4
|
-
var ws$1 = require('graphql-ws/lib/use/ws');
|
|
5
3
|
var compat = require('es-toolkit/compat');
|
|
4
|
+
var graphqlWs = require('graphql-ws');
|
|
5
|
+
var ws$1 = require('graphql-ws/use/ws');
|
|
6
6
|
var ws = require('ws');
|
|
7
7
|
var logging = require('../logging.js');
|
|
8
8
|
var utils = require('./utils.js');
|
|
@@ -76,11 +76,11 @@ function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext,
|
|
|
76
76
|
claims: ctx.extra.claims,
|
|
77
77
|
});
|
|
78
78
|
},
|
|
79
|
-
onOperation(_ctx,
|
|
80
|
-
logging.logSubscriptionOperation({ args, logLevel: operationLogLevel, resolveLogger: resolveSubscriptionOperationLogger });
|
|
79
|
+
onOperation(_ctx, id, _payload, args) {
|
|
80
|
+
logging.logSubscriptionOperation({ id, args, logLevel: operationLogLevel, resolveLogger: resolveSubscriptionOperationLogger });
|
|
81
81
|
},
|
|
82
|
-
onNext(_ctx,
|
|
83
|
-
logging.logSubscriptionOperation({ args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger });
|
|
82
|
+
onNext(_ctx, id, _payload, args, { data, ...result }) {
|
|
83
|
+
logging.logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger });
|
|
84
84
|
},
|
|
85
85
|
}, wsServer);
|
|
86
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/
|
|
1
|
+
{"version":3,"file":"server.js","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext } from './context'\nimport { extractTokenFromConnectionParams, getHost } from './utils'\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n return useServer(\n {\n schema,\n onError(_ctx, message, errors) {\n logger.error('GraphQL subscriptions server error', { message, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n try {\n const claims = await verifyToken(getHost(ctx.extra.request), token)\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n return true\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, resolveLogger: resolveSubscriptionOperationLogger })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n )\n}\n"],"names":["WebSocketServer","useServer","extractTokenFromConnectionParams","getHost","pick","CloseCode","logSubscriptionOperation"],"mappings":"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"}
|
package/subscriptions/server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CloseCode } from 'graphql-ws';
|
|
2
|
-
import { useServer } from 'graphql-ws/lib/use/ws';
|
|
3
1
|
import { pick } from 'es-toolkit/compat';
|
|
2
|
+
import { CloseCode } from 'graphql-ws';
|
|
3
|
+
import { useServer } from 'graphql-ws/use/ws';
|
|
4
4
|
import { WebSocketServer } from 'ws';
|
|
5
5
|
import { logSubscriptionOperation } from '../logging.mjs';
|
|
6
6
|
import { extractTokenFromConnectionParams, getHost } from './utils.mjs';
|
|
@@ -74,11 +74,11 @@ function useSubscriptionsServer({ schema, httpServer, createSubscriptionContext,
|
|
|
74
74
|
claims: ctx.extra.claims,
|
|
75
75
|
});
|
|
76
76
|
},
|
|
77
|
-
onOperation(_ctx,
|
|
78
|
-
logSubscriptionOperation({ args, logLevel: operationLogLevel, resolveLogger: resolveSubscriptionOperationLogger });
|
|
77
|
+
onOperation(_ctx, id, _payload, args) {
|
|
78
|
+
logSubscriptionOperation({ id, args, logLevel: operationLogLevel, resolveLogger: resolveSubscriptionOperationLogger });
|
|
79
79
|
},
|
|
80
|
-
onNext(_ctx,
|
|
81
|
-
logSubscriptionOperation({ args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger });
|
|
80
|
+
onNext(_ctx, id, _payload, args, { data, ...result }) {
|
|
81
|
+
logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger });
|
|
82
82
|
},
|
|
83
83
|
}, wsServer);
|
|
84
84
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/
|
|
1
|
+
{"version":3,"file":"server.mjs","sources":["../../src/subscriptions/server.ts"],"sourcesContent":["import type { Logger } from '@makerx/node-common'\nimport { pick } from 'es-toolkit/compat'\nimport type { GraphQLSchema } from 'graphql'\nimport { CloseCode } from 'graphql-ws'\nimport { useServer } from 'graphql-ws/use/ws'\nimport type { Server } from 'http'\nimport { WebSocketServer } from 'ws'\nimport type { GraphQLContext, JwtPayload } from '../context'\nimport { logSubscriptionOperation } from '../logging'\nimport type { CreateSubscriptionContext } from './context'\nimport { extractTokenFromConnectionParams, getHost } from './utils'\n\nexport function useSubscriptionsServer<TLogger extends Logger = Logger>({\n schema,\n httpServer,\n createSubscriptionContext,\n logger,\n operationLogLevel = 'info',\n path = '/graphql',\n verifyToken,\n requireAuth,\n jwtClaimsToLog = ['oid', 'iss'],\n resolveSubscriptionOperationLogger,\n}: {\n schema: GraphQLSchema\n httpServer: Server\n createSubscriptionContext: CreateSubscriptionContext\n logger: TLogger\n operationLogLevel?: keyof TLogger\n path?: string\n verifyToken?: (host: string, token: string) => Promise<JwtPayload>\n requireAuth?: boolean\n jwtClaimsToLog?: string[]\n resolveSubscriptionOperationLogger?: (context: GraphQLContext) => TLogger\n}) {\n if (requireAuth && !verifyToken) throw new Error('verifyToken must be supplied when requireAuth is true')\n\n const wsServer = new WebSocketServer({\n server: httpServer,\n path,\n })\n\n return useServer(\n {\n schema,\n onError(_ctx, message, errors) {\n logger.error('GraphQL subscriptions server error', { message, errors })\n },\n onConnect: async (ctx) => {\n const connectionEstablished = 'Subscription connection established'\n if (!verifyToken) {\n logger.info(connectionEstablished)\n return true\n }\n\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n return false\n }\n logger.info(connectionEstablished)\n return true\n }\n\n try {\n const claims = await verifyToken(getHost(ctx.extra.request), token)\n ctx.extra.claims = claims as unknown as undefined\n logger.info(connectionEstablished, {\n claims: pick(claims, jwtClaimsToLog),\n })\n return true\n } catch (error) {\n logger.error('Failed to verify subscription connection auth token', { error })\n return false\n }\n },\n onSubscribe: async (ctx) => {\n if (!verifyToken) return\n const token = extractTokenFromConnectionParams(ctx.connectionParams)\n if (!token) {\n if (requireAuth) {\n logger.error('No authorization parameter was supplied via websocket connection params')\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n return\n }\n try {\n await verifyToken(getHost(ctx.extra.request), token)\n } catch (error) {\n logger.warn('Subscription connection auth token is no longer valid', { claims: pick(ctx.extra.claims, jwtClaimsToLog), error })\n ctx.extra.socket.close(CloseCode.Forbidden, 'Forbidden')\n }\n },\n onDisconnect({ extra: { claims } }) {\n logger.info('Subscription connection disconnected', { claims: pick(claims, jwtClaimsToLog) })\n },\n context: async (ctx) => {\n return createSubscriptionContext({\n connectRequest: ctx.extra.request,\n connectionParams: ctx.connectionParams,\n claims: ctx.extra.claims as JwtPayload | undefined,\n })\n },\n onOperation(_ctx, id, _payload, args) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, resolveLogger: resolveSubscriptionOperationLogger })\n },\n onNext(_ctx, id, _payload, args, { data, ...result }) {\n logSubscriptionOperation({ id, args, logLevel: operationLogLevel, result, resolveLogger: resolveSubscriptionOperationLogger })\n },\n },\n wsServer,\n )\n}\n"],"names":[],"mappings":"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"}
|