Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,13 +236,15 @@ Convenience builders that read `ctx.user.roles` / `ctx.user.scopes`. All use gra

### createRule

Typed wrapper around graphql-shield's `rule(...)` for ad-hoc rules. Unlike the underlying `rule`, `createRule` types `parent`, `args` and `ctx` to your generics.
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`.

```ts
const isOwner = createRule<GraphQLContext, Record, { ownerId: string }>((parent, _, ctx) => parent.ownerId === ctx.user?.id)
const isOwner = createRule<GraphQLContext, { ownerId: string }>()((parent, _, ctx) => parent.ownerId === ctx.user?.id)

const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })((_, __, ctx) => ctx.user?.roles.includes('admin') === true)
```

Defaults to `'strict'` cache; pass `'contextual'` as the second argument for rules whose result depends only on `ctx`.
Generics are `<TContext, TParent = unknown, TArgs = unknown>`. The `name` and `options` arguments are forwarded verbatim to graphql-shield's `rule`.

### combineRuleWithAll

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@makerx/graphql-core",
"version": "3.0.0-beta.6",
"version": "3.0.0-beta.7",
"private": false,
"description": "A set of core GraphQL utilities that MakerX uses to build GraphQL APIs",
"author": "MakerX",
Expand Down
24 changes: 24 additions & 0 deletions src/request-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,44 @@ import type { Request } from 'express'
import type { IncomingMessage } from 'http'
import type { TLSSocket } from 'tls'

/**
* Normalised metadata describing an inbound HTTP request or WebSocket upgrade, used as a
* common shape for logging, tracing, and request-scoped context across HTTP and subscription
* transports. Extends `Record<string, unknown>` so consumers can augment it with additional
* fields without losing type compatibility.
*/
export interface BaseRequestInfo extends Record<string, unknown> {
/** Unique identifier for the request, taken from the `x-request-id` header or generated as a UUID. */
requestId: string
/** Transport that produced the request: `http` for Express requests, `subscription` for WebSocket upgrades. */
source: 'http' | 'subscription'
/** Resolved scheme of the request, accounting for TLS termination and `x-forwarded-proto`. */
protocol: 'http' | 'https' | 'ws' | 'wss'
/** Hostname resolved from `x-forwarded-host`, the `host` header, or the Express hostname fallback. */
host: string
/** Port resolved from the host header, omitted when it matches the default for the protocol. */
port?: number
/** HTTP method (e.g. `GET`, `POST`); empty string when not provided on the underlying request. */
method: string
/** Base URL in the form `protocol://host[:port]`, with default ports omitted. */
baseUrl: string
/**
* Request path with query string (not an absolute URL), e.g. `/graphql?op=Foo`. Sourced from
* Express `req.originalUrl` for HTTP (preserved across router mounts/rewrites) or raw `req.url`
* for WebSocket upgrades. Combine with `baseUrl` to form an absolute URL.
*/
url: string
/** Value of the `origin` header, or empty string when absent. */
origin: string
/** Value of the `referer` header, when present. */
referer?: string
/** Value of the `x-correlation-id` header for cross-service request correlation, when present. */
correlationId?: string
/** Azure Application Request Routing log id from the `x-arr-log-id` header, when present. */
arrLogId?: string
/** Client IP from the first `x-forwarded-for` entry, falling back to the socket remote address. */
clientIp?: string
/** Value of the `user-agent` header, when present. */
userAgent?: string
}

Expand Down
34 changes: 19 additions & 15 deletions src/shield.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { GraphQLResolveInfo } from 'graphql'
import type { allow, and, chain, IRules, or, race } from 'graphql-shield'
import { rule, shield } from 'graphql-shield'
import type { GraphQLContext } from './context'
Expand All @@ -10,29 +11,32 @@ type RuleCombinator = typeof chain | typeof race | typeof or | typeof and
// what it does export.
export type ShieldRule = ReturnType<(typeof allow)['getRules']>[number]

type RuleConstructorOptions = NonNullable<Parameters<typeof rule>[1]>
type RuleResult = boolean | string | Error
type ShieldRuleFn = Parameters<ReturnType<typeof rule>>[0]

/**
* Thin typed wrapper around graphql-shield's `rule(...)` for building ad-hoc shield rules.
*
* Lets you pass a plain predicate (sync or async, or returning an `Error`) and get back a
* {@link ShieldRule} with the `parent`, `args`, and `ctx` arguments typed to your generics —
* graphql-shield's native `rule` types these as `any`.
*
* Defaults the rule's cache to `'strict'`; use `'contextual'` when the result depends only on
* `ctx` (e.g. the current user), so it can be reused across fields in the same request.
* Strongly typed wrapper around graphql-shield's `rule(...)`.
*
* @param logic Predicate returning `true` to allow, `false` to deny, or an `Error` to deny with a specific error.
* @param cache graphql-shield cache strategy. `'strict'` (default) keys on parent+args+ctx; `'contextual'` keys on ctx only.
* Mirrors the underlying `rule` call signature — `createRule(name?, options?)(fn)` — with the
* same parameter order and meanings. The only difference is that `parent`, `args`, and `ctx` on
* `fn` are typed via the generics instead of `any`. Returns the same {@link ShieldRule} that the
* underlying `rule` produces.
*
* Usage:
*
* const isOwner = createRule<GraphQLContext, { ownerId: string }>(
* const isOwner = createRule<GraphQLContext, { ownerId: string }>()(
* (parent, _, ctx) => parent.ownerId === ctx.user?.id,
* )
*
* const isAdmin = createRule<GraphQLContext>('isAdmin', { cache: 'contextual' })(
* (_, __, ctx) => ctx.user?.roles.includes('admin') === true,
* )
*/
export const createRule = <TContext, TParent = unknown, TArgs = unknown>(
logic: (parent: TParent, args: TArgs, ctx: TContext) => boolean | Promise<boolean> | Error,
cache: 'contextual' | 'strict' = 'strict',
): ShieldRule => rule({ cache })(async (parent, args, ctx) => logic(parent, args, ctx))
export 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 =>
rule(name, options)(fn as ShieldRuleFn)

/**
* Combines a given rule with all existing defined rules using the provided combinator
Expand Down
Loading