@edium/halifax 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import { type AuthContext, type AuthStrategy } from '../auth/AuthStrategy.js';
2
2
  import { type CacheStore } from '../core/cache/index.js';
3
3
  import { type OpenApiOptions } from '../openapi/index.js';
4
+ import { type GraphQLOptions } from '../graphql/index.js';
4
5
  import { type ResourceDefinition } from '../core/types.js';
5
6
  import type { HttpRequest, HttpServer } from '../core/types.js';
6
7
  import { normalizeError } from '../core/handlerUtils.js';
@@ -40,6 +41,29 @@ export interface TenantOptions {
40
41
  * cross-tenant ("god mode") access for callers with no tenant.
41
42
  */
42
43
  strict?: boolean;
44
+ /**
45
+ * Roles or permission slugs whose holders may bypass tenant scoping for **read** operations
46
+ * (`getOne`, `getMany`, and the query builder), allowing them to see records across all tenants.
47
+ * Any single match in `auth.roles` or `auth.permissions` grants the bypass.
48
+ *
49
+ * When a bypass caller wants to see only one tenant's data they use the normal filter
50
+ * mechanism — `?companyId=42` on REST or `filter: { companyId: 42 }` in GraphQL.
51
+ * No special header or query parameter is needed; the tenant field is just another filterable
52
+ * column from the admin's perspective.
53
+ *
54
+ * Write operations (create / update / delete) are **never** bypassed: the tenant value
55
+ * continues to come from `resolveId`, keeping write provenance tied to auth — never to
56
+ * client-supplied input. An admin whose token carries no tenant will receive 403 on writes
57
+ * unless `strict` is `false`.
58
+ *
59
+ * Per-resource {@link ResourceDefinition.bypassTenantRoles} takes precedence over this list.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * bypassRoles: ['super_admin', 'support:read-all']
64
+ * ```
65
+ */
66
+ bypassRoles?: string[];
43
67
  }
44
68
  /** Options for {@link registerCrudApi} / {@link createExpressCrudRouter}. */
45
69
  export interface CrudApiOptions {
@@ -60,6 +84,20 @@ export interface CrudApiOptions {
60
84
  * additional routes: `GET /openapi.json` (raw spec) and `GET /docs` (Swagger UI).
61
85
  */
62
86
  openapi?: OpenApiOptions;
87
+ /**
88
+ * Enable a GraphQL endpoint. GraphQL is **disabled by default** — you must set
89
+ * `enabled: true` to activate it. When enabled, Halifax registers `POST <path>` (execution)
90
+ * and optionally `GET <path>` (GraphiQL IDE). The schema is auto-generated from all
91
+ * resources that have `graphql !== false`. Requires the `graphql` peer dependency.
92
+ *
93
+ * See [README_GRAPHQL.md](./README_GRAPHQL.md) for full docs and examples.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * graphql: { enabled: true, path: '/graphql', graphiql: true }
98
+ * ```
99
+ */
100
+ graphql?: GraphQLOptions;
63
101
  /**
64
102
  * API-wide read-through caching. Provide a `store` (defaults to an in-process
65
103
  * {@link InMemoryCacheStore}) and/or a default `ttlSeconds` applied to every resource that
@@ -1,6 +1,8 @@
1
1
  import { AllowAllAuthStrategy } from '../auth/AuthStrategy.js';
2
+ import { checkRequiredPermissions } from '../auth/strategies/types.js';
2
3
  import { createCachingRepository, InMemoryCacheStore } from '../core/cache/index.js';
3
4
  import { generateOpenApiSpec, generateDocsHtml } from '../openapi/index.js';
5
+ import { registerGraphqlRoute } from '../graphql/index.js';
4
6
  import { defaultCrudPermissions } from '../core/types.js';
5
7
  import { ServerError } from '../errors/ServerError.js';
6
8
  import { AuthorizationError } from '../errors/AuthorizationError.js';
@@ -84,6 +86,8 @@ function effectiveTenantField(resource, tenant) {
84
86
  }
85
87
  /** Matches a safe SQL identifier — tenant fields are interpolated into SQL on bulk paths. */
86
88
  const safeIdentifier = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
89
+ /** Read-only actions that admin bypass applies to. Writes always enforce tenant scoping. */
90
+ const READ_ACTIONS = new Set(['readOne', 'readMany', 'readManyWithQueryBuilder']);
87
91
  /**
88
92
  * Registers all CRUD routes for every resource on the given HTTP server.
89
93
  *
@@ -132,10 +136,16 @@ export function registerCrudApi(server, resources, options = {}) {
132
136
  bust
133
137
  })
134
138
  : repo;
135
- const resolveRepo = async (req, auth) => {
139
+ const resolveRepo = async (req, auth, action) => {
136
140
  const bust = cachingEnabled && wantsCacheBust(req, bustHeader);
137
141
  if (!tenantField || !options.tenant)
138
142
  return withCache(repository, 'global', bust);
143
+ // Admin bypass: callers with a privileged role/permission get unscoped reads.
144
+ // Writes always fall through to resolveId — tenant on writes comes from auth, never bypass.
145
+ const bypassRoles = resource.bypassTenantRoles ?? options.tenant.bypassRoles ?? [];
146
+ if (READ_ACTIONS.has(action) && bypassRoles.length > 0 && checkRequiredPermissions(auth, bypassRoles)) {
147
+ return withCache(repository, 'global', bust);
148
+ }
139
149
  const value = await options.tenant.resolveId({ auth, req, resource });
140
150
  if (value === undefined || value === null || value === '') {
141
151
  if (options.tenant.strict !== false)
@@ -199,6 +209,48 @@ export function registerCrudApi(server, resources, options = {}) {
199
209
  });
200
210
  }
201
211
  });
212
+ // ─── GraphQL endpoint ──────────────────────────────────────────────────────
213
+ if (options.graphql?.enabled === true) {
214
+ // Build per-resource contexts carrying the already-resolved resolveRepo closures.
215
+ // We re-iterate resources to capture the closure variables that were set up above.
216
+ const graphqlContexts = resources.map((rawResource) => {
217
+ const resource = normalizeResource(rawResource);
218
+ const repository = rawResource.repository;
219
+ const tenantField = effectiveTenantField(resource, options.tenant);
220
+ const cacheTtl = resource.cache === false
221
+ ? undefined
222
+ : (resource.cache?.ttlSeconds ?? options.cache?.ttlSeconds);
223
+ const cachingEnabled = cacheTtl !== undefined;
224
+ const withCacheLocal = (repo, scopeKey, bust) => cachingEnabled
225
+ ? createCachingRepository(repo, {
226
+ store: cacheStore,
227
+ ttlSeconds: cacheTtl,
228
+ namespace: `${resource.name}:${scopeKey}`,
229
+ bust
230
+ })
231
+ : repo;
232
+ const resolveRepoLocal = async (req, auth, action) => {
233
+ const bust = cachingEnabled && wantsCacheBust(req, bustHeader);
234
+ if (!tenantField || !options.tenant)
235
+ return withCacheLocal(repository, 'global', bust);
236
+ const bypassRoles = resource.bypassTenantRoles ?? options.tenant.bypassRoles ?? [];
237
+ if (READ_ACTIONS.has(action) && bypassRoles.length > 0 && checkRequiredPermissions(auth, bypassRoles)) {
238
+ return withCacheLocal(repository, 'global', bust);
239
+ }
240
+ const value = await options.tenant.resolveId({ auth, req, resource });
241
+ if (value === undefined || value === null || value === '') {
242
+ if (options.tenant.strict !== false)
243
+ throw new AuthorizationError('No tenant is associated with this request.');
244
+ return withCacheLocal(repository, 'global', bust);
245
+ }
246
+ return withCacheLocal(repository.withScope({ field: tenantField, value }), String(value), bust);
247
+ };
248
+ const hooks = resource.hooks;
249
+ return { resource, authStrategy, hooks, resolveRepo: resolveRepoLocal };
250
+ });
251
+ registerGraphqlRoute(server, graphqlContexts, options.graphql, authStrategy);
252
+ }
253
+ // ─── OpenAPI spec + docs ───────────────────────────────────────────────────
202
254
  if (options.openapi && options.openapi.enabled !== false) {
203
255
  const specPath = options.openapi.specPath ?? '/openapi.json';
204
256
  const docsPath = options.openapi.docsPath ?? '/docs';
@@ -72,5 +72,5 @@ export interface RouteHandlerContext {
72
72
  authStrategy: AuthStrategy;
73
73
  envelope: string | null;
74
74
  hooks: CrudHooks<Record<string, unknown>, Record<string, unknown>, Record<string, unknown>> | undefined;
75
- resolveRepo: (req: HttpRequest, auth: AuthContext) => Promise<Repository>;
75
+ resolveRepo: (req: HttpRequest, auth: AuthContext, action: CrudAction) => Promise<Repository>;
76
76
  }
@@ -3,7 +3,7 @@ export function registerCreate(server, basePath, ctx) {
3
3
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
4
4
  server.registerRoute('POST', basePath, wrap(async (req, res) => {
5
5
  const auth = await authorizeRequest(req, resource, 'create', authStrategy);
6
- const repo = await resolveRepo(req, auth);
6
+ const repo = await resolveRepo(req, auth, 'create');
7
7
  const idempotencyKey = getHeaderValue(req, 'idempotency-key');
8
8
  const createOptions = idempotencyKey ? { idempotencyKey } : undefined;
9
9
  const hookCtx = { auth, resource, req };
@@ -6,7 +6,7 @@ export function registerDeleteMany(server, basePath, ctx) {
6
6
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
7
7
  server.registerRoute('DELETE', basePath, wrap(async (req, res) => {
8
8
  const auth = await authorizeRequest(req, resource, 'deleteMany', authStrategy);
9
- const repo = await resolveRepo(req, auth);
9
+ const repo = await resolveRepo(req, auth, 'deleteMany');
10
10
  if (!repo.deleteMany)
11
11
  throw new NotImplementedError('This resource does not support deleteMany.');
12
12
  const hookCtx = { auth, resource, req };
@@ -4,7 +4,7 @@ export function registerDeleteOne(server, basePath, ctx) {
4
4
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
5
5
  server.registerRoute('DELETE', `${basePath}/:id`, wrap(async (req, res) => {
6
6
  const auth = await authorizeRequest(req, resource, 'deleteOne', authStrategy);
7
- const repo = await resolveRepo(req, auth);
7
+ const repo = await resolveRepo(req, auth, 'deleteOne');
8
8
  const id = parseId(req.params['id']);
9
9
  const hookCtx = { auth, resource, req };
10
10
  if (hooks?.beforeDeleteOne)
@@ -5,7 +5,7 @@ export function registerQuery(server, basePath, queryBuilderPath, ctx) {
5
5
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
6
6
  server.registerRoute('POST', `${basePath}/${queryBuilderPath}`, wrap(async (req, res) => {
7
7
  const auth = await authorizeRequest(req, resource, 'readManyWithQueryBuilder', authStrategy);
8
- const repo = await resolveRepo(req, auth);
8
+ const repo = await resolveRepo(req, auth, 'readManyWithQueryBuilder');
9
9
  if (!repo.executeQuery)
10
10
  throw new NotImplementedError('This resource does not support the query builder.');
11
11
  const hookCtx = { auth, resource, req };
@@ -4,7 +4,7 @@ export function registerReadMany(server, basePath, ctx) {
4
4
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
5
5
  server.registerRoute('GET', basePath, wrap(async (req, res) => {
6
6
  const auth = await authorizeRequest(req, resource, 'readMany', authStrategy);
7
- const repo = await resolveRepo(req, auth);
7
+ const repo = await resolveRepo(req, auth, 'readMany');
8
8
  const hookCtx = { auth, resource, req };
9
9
  const parsedOptions = parseListOptions(req.query, resource);
10
10
  const listOptions = await applyHook(hooks?.beforeReadMany, parsedOptions, hookCtx);
@@ -5,7 +5,7 @@ export function registerReadOne(server, basePath, ctx) {
5
5
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
6
6
  server.registerRoute('GET', `${basePath}/:id`, wrap(async (req, res) => {
7
7
  const auth = await authorizeRequest(req, resource, 'readOne', authStrategy);
8
- const repo = await resolveRepo(req, auth);
8
+ const repo = await resolveRepo(req, auth, 'readOne');
9
9
  const id = parseId(req.params['id']);
10
10
  const hookCtx = { auth, resource, req };
11
11
  if (hooks?.beforeReadOne)
@@ -6,7 +6,7 @@ export function registerUpdateMany(server, basePath, ctx) {
6
6
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
7
7
  server.registerRoute('PATCH', basePath, wrap(async (req, res) => {
8
8
  const auth = await authorizeRequest(req, resource, 'updateMany', authStrategy);
9
- const repo = await resolveRepo(req, auth);
9
+ const repo = await resolveRepo(req, auth, 'updateMany');
10
10
  if (!repo.updateMany)
11
11
  throw new NotImplementedError('This resource does not support updateMany.');
12
12
  const hookCtx = { auth, resource, req };
@@ -4,7 +4,7 @@ export function registerUpdateOne(server, basePath, ctx) {
4
4
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
5
5
  server.registerRoute('PATCH', `${basePath}/:id`, wrap(async (req, res) => {
6
6
  const auth = await authorizeRequest(req, resource, 'updateOne', authStrategy);
7
- const repo = await resolveRepo(req, auth);
7
+ const repo = await resolveRepo(req, auth, 'updateOne');
8
8
  const id = parseId(req.params['id']);
9
9
  const hookCtx = { auth, resource, req };
10
10
  const rawBody = filterWritableFields(resource, (req.body ?? {}), auth);
@@ -4,7 +4,7 @@ export function registerUpsertOne(server, basePath, ctx) {
4
4
  const { resource, authStrategy, envelope, hooks, resolveRepo } = ctx;
5
5
  server.registerRoute('PUT', `${basePath}/:id`, wrap(async (req, res) => {
6
6
  const auth = await authorizeRequest(req, resource, 'upsertOne', authStrategy);
7
- const repo = await resolveRepo(req, auth);
7
+ const repo = await resolveRepo(req, auth, 'upsertOne');
8
8
  if (!repo.upsertOne)
9
9
  throw new NotImplementedError('This resource does not support upsert.');
10
10
  const id = parseId(req.params['id']);
@@ -389,6 +389,28 @@ export interface ResourceDefinition<TRecord = unknown, TCreate = Partial<TRecord
389
389
  * `false` to explicitly disable caching for this resource even when a default is configured.
390
390
  */
391
391
  cache?: ResourceCacheConfig | false;
392
+ /**
393
+ * Controls whether this resource is exposed through the GraphQL endpoint.
394
+ * Set to `false` to exclude this resource from the generated GraphQL schema entirely.
395
+ * Defaults to `true` when GraphQL is enabled on the API.
396
+ */
397
+ graphql?: boolean;
398
+ /**
399
+ * Roles or permission slugs whose holders may bypass tenant scoping for **read** operations
400
+ * on this resource, allowing them to see records across all tenants. Any single match in
401
+ * `auth.roles` or `auth.permissions` grants the bypass.
402
+ *
403
+ * Bypass callers receive all records on reads. To narrow to a single tenant they use the
404
+ * standard filter mechanism (`?companyId=42` on REST, `filter: { companyId: 42 }` in
405
+ * GraphQL) — the tenant field is just another filterable column from their perspective.
406
+ *
407
+ * Write operations (create / update / delete) are **never** bypassed — tenant value on writes
408
+ * always comes from `resolveId` (the auth token), not from the bypass path.
409
+ *
410
+ * Overrides {@link TenantOptions.bypassRoles} for this resource. Set to `[]` to disable
411
+ * bypass on this resource even when a global `bypassRoles` is configured.
412
+ */
413
+ bypassTenantRoles?: string[];
392
414
  /**
393
415
  * Wrap every success response body for this resource under a single key
394
416
  * (e.g. `'data'` →
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generates an HTML page that renders the GraphiQL IDE, loading assets from unpkg.com CDN.
3
+ * The GraphQL endpoint URL is embedded so GraphiQL points at the correct server path.
4
+ */
5
+ export declare function generateGraphiQLHtml(graphqlPath: string, title: string): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Generates an HTML page that renders the GraphiQL IDE, loading assets from unpkg.com CDN.
3
+ * The GraphQL endpoint URL is embedded so GraphiQL points at the correct server path.
4
+ */
5
+ export function generateGraphiQLHtml(graphqlPath, title) {
6
+ const escapedPath = JSON.stringify(graphqlPath);
7
+ return `<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="utf-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
12
+ <title>${title}</title>
13
+ <link href="https://unpkg.com/graphiql/graphiql.min.css" rel="stylesheet" />
14
+ <style>body { margin: 0; }</style>
15
+ </head>
16
+ <body>
17
+ <div id="graphiql" style="height:100vh;"></div>
18
+ <script crossorigin src="https://unpkg.com/react/umd/react.production.min.js"></script>
19
+ <script crossorigin src="https://unpkg.com/react-dom/umd/react-dom.production.min.js"></script>
20
+ <script src="https://unpkg.com/graphiql/graphiql.min.js" type="application/javascript"></script>
21
+ <script>
22
+ var fetcher = GraphiQL.createFetcher({ url: ${escapedPath} });
23
+ ReactDOM.createRoot(document.getElementById('graphiql')).render(
24
+ React.createElement(GraphiQL, { fetcher: fetcher })
25
+ );
26
+ </script>
27
+ </body>
28
+ </html>`;
29
+ }
@@ -0,0 +1,4 @@
1
+ export type { GraphQLOptions, GraphQLResourceContext, GraphQLResolverContext } from './types.js';
2
+ export { buildGraphQLSchema } from './schema.js';
3
+ export { registerGraphqlRoute } from './registerGraphqlRoute.js';
4
+ export { generateGraphiQLHtml } from './graphiql.js';
@@ -0,0 +1,3 @@
1
+ export { buildGraphQLSchema } from './schema.js';
2
+ export { registerGraphqlRoute } from './registerGraphqlRoute.js';
3
+ export { generateGraphiQLHtml } from './graphiql.js';
@@ -0,0 +1,10 @@
1
+ import type { GraphQLOptions, GraphQLResourceContext } from './types.js';
2
+ import type { AuthStrategy } from '../auth/strategies/types.js';
3
+ import type { HttpServer } from '../core/types.js';
4
+ /**
5
+ * Registers the `POST /graphql` execution endpoint and optionally the `GET /graphql`
6
+ * GraphiQL IDE page on the given HTTP server.
7
+ *
8
+ * The schema is built once at registration time from the provided resource contexts.
9
+ */
10
+ export declare function registerGraphqlRoute(server: HttpServer, contexts: GraphQLResourceContext[], options: GraphQLOptions, authStrategy: AuthStrategy): void;
@@ -0,0 +1,79 @@
1
+ import { graphql, parse, validate } from 'graphql';
2
+ import { buildGraphQLSchema } from './schema.js';
3
+ import { generateGraphiQLHtml } from './graphiql.js';
4
+ import { sendError } from '../core/handlerUtils.js';
5
+ /**
6
+ * Registers the `POST /graphql` execution endpoint and optionally the `GET /graphql`
7
+ * GraphiQL IDE page on the given HTTP server.
8
+ *
9
+ * The schema is built once at registration time from the provided resource contexts.
10
+ */
11
+ export function registerGraphqlRoute(server, contexts, options, authStrategy) {
12
+ if (options.enabled === false)
13
+ return;
14
+ const path = options.path ?? '/graphql';
15
+ const graphiqlEnabled = options.graphiql !== false;
16
+ const requireAuth = options.requireAuth === true;
17
+ const title = options.title ?? 'Halifax GraphQL';
18
+ const schema = buildGraphQLSchema(contexts);
19
+ const graphiqlHtml = graphiqlEnabled ? generateGraphiQLHtml(path, title) : null;
20
+ // ─── POST /graphql — execution endpoint ─────────────────────────────────
21
+ server.registerRoute('POST', path, async (req, res) => {
22
+ try {
23
+ if (requireAuth)
24
+ await authStrategy.authenticate(req);
25
+ const body = (req.body ?? {});
26
+ const source = body.query ?? '';
27
+ if (!source.trim()) {
28
+ await res.status(400).json({
29
+ errors: [{ message: 'GraphQL request must include a query.' }]
30
+ });
31
+ return;
32
+ }
33
+ // Parse and validate before executing so syntax errors surface cleanly.
34
+ let document;
35
+ try {
36
+ document = parse(source);
37
+ }
38
+ catch (syntaxError) {
39
+ await res.status(400).json({
40
+ errors: [
41
+ { message: syntaxError instanceof Error ? syntaxError.message : 'Syntax error.' }
42
+ ]
43
+ });
44
+ return;
45
+ }
46
+ const validationErrors = validate(schema, document);
47
+ if (validationErrors.length) {
48
+ await res.status(400).json({ errors: validationErrors.map((e) => ({ message: e.message })) });
49
+ return;
50
+ }
51
+ const result = await graphql({
52
+ schema,
53
+ source,
54
+ variableValues: body.variables,
55
+ operationName: body.operationName,
56
+ contextValue: { req }
57
+ });
58
+ res.setHeader?.('Content-Type', 'application/json');
59
+ await res.status(200).json(result);
60
+ }
61
+ catch (error) {
62
+ await sendError(error, res);
63
+ }
64
+ });
65
+ // ─── GET /graphql — GraphiQL IDE ─────────────────────────────────────────
66
+ if (graphiqlEnabled && graphiqlHtml) {
67
+ server.registerRoute('GET', path, async (req, res) => {
68
+ try {
69
+ if (requireAuth)
70
+ await authStrategy.authenticate(req);
71
+ res.setHeader?.('Content-Type', 'text/html; charset=utf-8');
72
+ res.send?.(graphiqlHtml);
73
+ }
74
+ catch (error) {
75
+ await sendError(error, res);
76
+ }
77
+ });
78
+ }
79
+ }
@@ -0,0 +1,6 @@
1
+ import { GraphQLScalarType } from 'graphql';
2
+ /**
3
+ * Custom `JSON` scalar that accepts any JSON-serializable value.
4
+ * Used for filter `value1`/`value2` fields in QueryFilterInput and for `object` field types.
5
+ */
6
+ export declare const GraphQLJSON: GraphQLScalarType<unknown, unknown>;
@@ -0,0 +1,32 @@
1
+ import { GraphQLScalarType, Kind } from 'graphql';
2
+ function parseLiteralToValue(ast) {
3
+ switch (ast.kind) {
4
+ case Kind.STRING:
5
+ return ast.value;
6
+ case Kind.BOOLEAN:
7
+ return ast.value;
8
+ case Kind.INT:
9
+ return parseInt(ast.value, 10);
10
+ case Kind.FLOAT:
11
+ return parseFloat(ast.value);
12
+ case Kind.NULL:
13
+ return null;
14
+ case Kind.LIST:
15
+ return ast.values.map(parseLiteralToValue);
16
+ case Kind.OBJECT:
17
+ return Object.fromEntries(ast.fields.map((f) => [f.name.value, parseLiteralToValue(f.value)]));
18
+ default:
19
+ return null;
20
+ }
21
+ }
22
+ /**
23
+ * Custom `JSON` scalar that accepts any JSON-serializable value.
24
+ * Used for filter `value1`/`value2` fields in QueryFilterInput and for `object` field types.
25
+ */
26
+ export const GraphQLJSON = new GraphQLScalarType({
27
+ name: 'JSON',
28
+ description: 'Arbitrary JSON value (string, number, boolean, null, array, or object).',
29
+ serialize: (value) => value,
30
+ parseValue: (value) => value,
31
+ parseLiteral: parseLiteralToValue
32
+ });
@@ -0,0 +1,3 @@
1
+ import { GraphQLSchema } from 'graphql';
2
+ import type { GraphQLResourceContext } from './types.js';
3
+ export declare function buildGraphQLSchema(contexts: GraphQLResourceContext[]): GraphQLSchema;