@loradb/lora-graphql 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -13
- package/dist/analyze/access-lint.d.ts +7 -0
- package/dist/analyze/access.d.ts +13 -33
- package/dist/analyze/assert-schema.d.ts +19 -0
- package/dist/analyze/diff.d.ts +1 -1
- package/dist/analyze/plans.d.ts +1 -1
- package/dist/analyze/principals.d.ts +25 -0
- package/dist/analyze/verdicts.d.ts +51 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/compile/cache.d.ts +1 -1
- package/dist/compile/read/abstract.d.ts +19 -0
- package/dist/compile/read/aggregates.d.ts +34 -0
- package/dist/compile/read/by-keys.d.ts +10 -0
- package/dist/compile/read/common.d.ts +30 -0
- package/dist/compile/read/connection.d.ts +26 -0
- package/dist/compile/read/cypher-field.d.ts +21 -0
- package/dist/compile/read/match.d.ts +11 -0
- package/dist/compile/read/project.d.ts +15 -0
- package/dist/compile/read/root.d.ts +5 -0
- package/dist/compile/read/search.d.ts +16 -0
- package/dist/compile/read/sort.d.ts +42 -0
- package/dist/compile/read/types.d.ts +61 -0
- package/dist/{diff-B9iDdELF.js → diff-K8-GSeKH.js} +2 -2
- package/dist/diff-K8-GSeKH.js.map +1 -0
- package/dist/{driver-C5dSoSD8.js → driver-CXKPLfkz.js} +10796 -10254
- package/dist/driver-CXKPLfkz.js.map +1 -0
- package/dist/execute/cypher-mutation.d.ts +1 -1
- package/dist/execute/mutate/adjust.d.ts +5 -0
- package/dist/execute/mutate/checks.d.ts +21 -0
- package/dist/execute/mutate/delete.d.ts +7 -0
- package/dist/execute/mutate/env.d.ts +73 -0
- package/dist/execute/mutate/errors.d.ts +7 -0
- package/dist/execute/mutate/fragments.d.ts +37 -0
- package/dist/execute/mutate/plan.d.ts +95 -0
- package/dist/execute/mutate/runner.d.ts +129 -0
- package/dist/execute/mutate/unique-together.d.ts +18 -0
- package/dist/execute/mutate.d.ts +2 -79
- package/dist/index.d.ts +10 -6
- package/dist/index.js +2 -2
- package/dist/lora-graphql.d.ts +18 -328
- package/dist/model/build/authorization.d.ts +31 -0
- package/dist/model/build/cypher-field.d.ts +11 -0
- package/dist/model/build/directive-args.d.ts +16 -0
- package/dist/model/build/limits.d.ts +18 -0
- package/dist/model/build/relationship-field.d.ts +11 -0
- package/dist/model/build/rule-checks.d.ts +23 -0
- package/dist/model/build/rule-references.d.ts +28 -0
- package/dist/model/build/scalar-field.d.ts +7 -0
- package/dist/model/build/search.d.ts +4 -0
- package/dist/model/build/shapes.d.ts +18 -0
- package/dist/model/build/unique-together.d.ts +5 -0
- package/dist/model/build.d.ts +3 -20
- package/dist/model/directives.d.ts +1 -1
- package/dist/model/types.d.ts +12 -2
- package/dist/options.d.ts +255 -0
- package/dist/results.d.ts +70 -0
- package/dist/runtime/custom-requires.d.ts +7 -0
- package/dist/runtime/database-error.d.ts +10 -0
- package/dist/runtime/documents.d.ts +35 -0
- package/dist/runtime/limits.d.ts +37 -0
- package/dist/runtime/request.d.ts +33 -0
- package/dist/runtime/timing.d.ts +13 -0
- package/dist/schema/aggregates.d.ts +17 -0
- package/dist/schema/base.d.ts +17 -0
- package/dist/schema/build.d.ts +3 -42
- package/dist/schema/filters.d.ts +10 -0
- package/dist/schema/hooks.d.ts +42 -0
- package/dist/schema/objects.d.ts +36 -0
- package/dist/schema/query.d.ts +8 -0
- package/dist/schema/resolvers.d.ts +13 -0
- package/dist/schema/sorts.d.ts +5 -0
- package/dist/schema/subscriptions.d.ts +7 -0
- package/dist/subscriptions/change-hub.d.ts +50 -0
- package/dist/subscriptions/compiles.d.ts +23 -0
- package/dist/subscriptions/events.d.ts +9 -0
- package/dist/subscriptions/filter-cost.d.ts +15 -0
- package/dist/subscriptions/subscriptions.d.ts +73 -0
- package/dist/testing.d.ts +4 -1
- package/dist/testing.js +34 -29
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
- package/dist/compile/read.d.ts +0 -102
- package/dist/diff-B9iDdELF.js.map +0 -1
- package/dist/driver-C5dSoSD8.js.map +0 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GraphQLInputObjectType, GraphQLInputType } from 'graphql';
|
|
2
|
+
import { GraphModel, NodeType, RelationshipField, ScalarField } from '../model/types.js';
|
|
3
|
+
import { BaseTypes } from './base.js';
|
|
4
|
+
export declare function buildFilters(model: GraphModel, base: BaseTypes): {
|
|
5
|
+
inputTypeOf: (f: ScalarField) => GraphQLInputType;
|
|
6
|
+
whereOf: (typeName: string) => GraphQLInputObjectType;
|
|
7
|
+
polymorphic: (rel: RelationshipField) => boolean;
|
|
8
|
+
connectionWhere: (node: NodeType, rel: RelationshipField) => GraphQLInputObjectType;
|
|
9
|
+
};
|
|
10
|
+
export type Filters = ReturnType<typeof buildFilters>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { GraphQLFieldResolver, GraphQLResolveInfo, GraphQLScalarType } from 'graphql';
|
|
2
|
+
import { RootKind } from '../compile/read/types.js';
|
|
3
|
+
import { AbstractType, CustomField, CypherField, NodeType, ScalarField, SearchIndex } from '../model/types.js';
|
|
4
|
+
import { MutationResolver } from './mutations.js';
|
|
5
|
+
/** One event of a generated subscription. */
|
|
6
|
+
export interface ChangeEvent {
|
|
7
|
+
operation: "CREATE" | "UPDATE" | "DELETE" | "CONNECT" | "DISCONNECT";
|
|
8
|
+
key: unknown;
|
|
9
|
+
/** When the write was committed, ISO-8601. */
|
|
10
|
+
timestamp?: string | undefined;
|
|
11
|
+
/** For CONNECT / DISCONNECT: the relationship and its other end. */
|
|
12
|
+
relationship?: {
|
|
13
|
+
field: string;
|
|
14
|
+
type: string;
|
|
15
|
+
relatedType: string;
|
|
16
|
+
relatedKey: unknown;
|
|
17
|
+
} | undefined;
|
|
18
|
+
/** Stored properties before the write (`previousState: true`). */
|
|
19
|
+
previous?: Record<string, unknown> | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface SchemaHooks {
|
|
22
|
+
resolveAbstract: (abstract: AbstractType, info: GraphQLResolveInfo, context: unknown) => Promise<unknown>;
|
|
23
|
+
subscribe: (node: NodeType, args: Record<string, unknown>, context: unknown) => AsyncIterable<ChangeEvent>;
|
|
24
|
+
resolveChangedNode: (node: NodeType, event: ChangeEvent, info: GraphQLResolveInfo, context: unknown) => Promise<unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* A `previousState` value as the subscriber may see it: throws when
|
|
27
|
+
* field-level @authentication refuses it, applies masks by the claims.
|
|
28
|
+
*/
|
|
29
|
+
previousValue: (node: NodeType, field: ScalarField, value: unknown, context: unknown) => unknown;
|
|
30
|
+
/** Implementations of custom scalars, by name (the scalars option). */
|
|
31
|
+
scalars?: Readonly<Record<string, GraphQLScalarType>> | undefined;
|
|
32
|
+
/** The resolver of a `@customResolver` field (from the resolvers option). */
|
|
33
|
+
customResolver: (node: NodeType, field: CustomField) => GraphQLFieldResolver<Record<string, unknown>, unknown>;
|
|
34
|
+
resolveSearch: (node: NodeType, index: SearchIndex, info: GraphQLResolveInfo, context: unknown, connection: boolean) => Promise<unknown>;
|
|
35
|
+
resolveRoot: RootResolver;
|
|
36
|
+
resolveNode: NodeResolver;
|
|
37
|
+
resolveCypher: CypherRootResolver;
|
|
38
|
+
resolveMutation: MutationResolver;
|
|
39
|
+
}
|
|
40
|
+
export type CypherRootResolver = (field: CypherField, info: GraphQLResolveInfo, context: unknown) => Promise<unknown>;
|
|
41
|
+
export type RootResolver = (kind: RootKind, node: NodeType, info: GraphQLResolveInfo, context: unknown) => Promise<unknown>;
|
|
42
|
+
export type NodeResolver = (id: string, info: GraphQLResolveInfo, context: unknown) => Promise<unknown>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { GraphQLInputObjectType, GraphQLInterfaceType, GraphQLObjectType, GraphQLUnionType, GraphQLFieldConfigArgumentMap, GraphQLInputType, GraphQLOutputType } from 'graphql';
|
|
2
|
+
import { RawConnection } from '../compile/read/types.js';
|
|
3
|
+
import { CypherField, GraphModel } from '../model/types.js';
|
|
4
|
+
import { AggregateFieldType } from './aggregates.js';
|
|
5
|
+
import { BaseTypes } from './base.js';
|
|
6
|
+
import { Filters } from './filters.js';
|
|
7
|
+
import { SchemaHooks } from './hooks.js';
|
|
8
|
+
import { Sorts } from './sorts.js';
|
|
9
|
+
export declare function buildObjects(model: GraphModel, hooks: SchemaHooks, base: BaseTypes, filters: Filters, sorts: Sorts, aggregateFieldType: AggregateFieldType): {
|
|
10
|
+
objects: Map<string, GraphQLObjectType<any, any>>;
|
|
11
|
+
connections: Map<string, GraphQLObjectType<any, any>>;
|
|
12
|
+
abstractTypes: Map<string, GraphQLInterfaceType | GraphQLUnionType>;
|
|
13
|
+
plainObjects: Map<string, GraphQLObjectType<any, any>>;
|
|
14
|
+
connectionResolvers: {
|
|
15
|
+
edges: (src: RawConnection) => {
|
|
16
|
+
__sort: string;
|
|
17
|
+
node: unknown;
|
|
18
|
+
properties?: unknown;
|
|
19
|
+
__cursor: unknown[];
|
|
20
|
+
}[];
|
|
21
|
+
pageInfo: (src: RawConnection) => {
|
|
22
|
+
hasNextPage: boolean;
|
|
23
|
+
hasPreviousPage: boolean;
|
|
24
|
+
startCursor: string | null;
|
|
25
|
+
endCursor: string | null;
|
|
26
|
+
};
|
|
27
|
+
totalCount: (src: RawConnection & {
|
|
28
|
+
__denied?: number;
|
|
29
|
+
}) => number;
|
|
30
|
+
};
|
|
31
|
+
listArgs: (target: string) => GraphQLFieldConfigArgumentMap;
|
|
32
|
+
connectionArgs: (target: string, where?: GraphQLInputType, sort?: GraphQLInputObjectType) => GraphQLFieldConfigArgumentMap;
|
|
33
|
+
cypherOutput: (f: CypherField) => GraphQLOutputType;
|
|
34
|
+
cypherArgs: (f: CypherField) => GraphQLFieldConfigArgumentMap;
|
|
35
|
+
};
|
|
36
|
+
export type ObjectTypes = ReturnType<typeof buildObjects>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { GraphQLFieldConfigMap } from 'graphql';
|
|
2
|
+
import { GraphModel } from '../model/types.js';
|
|
3
|
+
import { AggregateTypes } from './aggregates.js';
|
|
4
|
+
import { BaseTypes } from './base.js';
|
|
5
|
+
import { Filters } from './filters.js';
|
|
6
|
+
import { SchemaHooks } from './hooks.js';
|
|
7
|
+
import { ObjectTypes } from './objects.js';
|
|
8
|
+
export declare function buildQuery(model: GraphModel, hooks: SchemaHooks, base: BaseTypes, filters: Filters, objectTypes: ObjectTypes, aggregateTypes: AggregateTypes): GraphQLFieldConfigMap<unknown, unknown>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GraphQLFieldResolver } from 'graphql';
|
|
2
|
+
import { RawConnection, RawEdge } from '../compile/read/types.js';
|
|
3
|
+
export type SortedEdge = RawEdge & {
|
|
4
|
+
__sort: string;
|
|
5
|
+
};
|
|
6
|
+
/** The rows of a page in the requested order. */
|
|
7
|
+
export declare function pageOf(src: RawConnection): RawEdge[];
|
|
8
|
+
/** Values under response keys: the compiler projects each alias. */
|
|
9
|
+
export declare const byResponseKey: GraphQLFieldResolver<Record<string, unknown>, unknown>;
|
|
10
|
+
/** A stored VECTOR as its numbers. */
|
|
11
|
+
export declare const vectorByResponseKey: GraphQLFieldResolver<Record<string, unknown>, unknown>;
|
|
12
|
+
/** Same, for nodes: fails on nodes a READ validate rule rejects. */
|
|
13
|
+
export declare const nodesByResponseKey: GraphQLFieldResolver<Record<string, unknown>, unknown>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { GraphQLInputObjectType } from 'graphql';
|
|
2
|
+
import { GraphModel } from '../model/types.js';
|
|
3
|
+
import { BaseTypes } from './base.js';
|
|
4
|
+
export declare function buildSorts(model: GraphModel, base: BaseTypes): Map<string, GraphQLInputObjectType>;
|
|
5
|
+
export type Sorts = ReturnType<typeof buildSorts>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { GraphQLFieldConfigMap } from 'graphql';
|
|
2
|
+
import { GraphModel } from '../model/types.js';
|
|
3
|
+
import { BaseTypes } from './base.js';
|
|
4
|
+
import { Filters } from './filters.js';
|
|
5
|
+
import { SchemaHooks } from './hooks.js';
|
|
6
|
+
import { ObjectTypes } from './objects.js';
|
|
7
|
+
export declare function buildSubscriptions(model: GraphModel, hooks: SchemaHooks, base: BaseTypes, filters: Filters, objectTypes: ObjectTypes): GraphQLFieldConfigMap<unknown, unknown>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { LoraDriver } from '../driver.js';
|
|
2
|
+
import { WriteChange } from '../execute/changes.js';
|
|
3
|
+
import { GraphModel } from '../model/types.js';
|
|
4
|
+
import { LoraGraphQLOptions } from '../options.js';
|
|
5
|
+
export interface ChangeHubOptions {
|
|
6
|
+
driver: LoraDriver;
|
|
7
|
+
model: GraphModel;
|
|
8
|
+
/** The `changeFeed` option. */
|
|
9
|
+
changeFeed: boolean | undefined;
|
|
10
|
+
/** The `maxQueuedChanges` option, defaulted. */
|
|
11
|
+
maxQueued: number;
|
|
12
|
+
onError: LoraGraphQLOptions["onError"];
|
|
13
|
+
/**
|
|
14
|
+
* The owning instance's public `onWrite`: `changes()` and the
|
|
15
|
+
* subscriptions register through it, like any other listener.
|
|
16
|
+
*/
|
|
17
|
+
onWrite: (listener: (change: WriteChange) => void) => () => void;
|
|
18
|
+
}
|
|
19
|
+
export declare class ChangeHub {
|
|
20
|
+
#private;
|
|
21
|
+
constructor(options: ChangeHubOptions);
|
|
22
|
+
/** Called after every committed mutation with its exact write-set. */
|
|
23
|
+
onWrite(listener: (change: WriteChange) => void): () => void;
|
|
24
|
+
/**
|
|
25
|
+
* Register a consumer of committed writes: of the engine feed when it
|
|
26
|
+
* is on, else of this instance's mutations. Returns its removal.
|
|
27
|
+
*/
|
|
28
|
+
listen(listener: (change: WriteChange) => void): () => unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Committed writes as an async iterator, e.g. for a subscription
|
|
31
|
+
* resolver. Only writes made through this instance are seen. `return()`
|
|
32
|
+
* or aborting `signal` (already aborted included) ends it at once.
|
|
33
|
+
*/
|
|
34
|
+
changes(options?: {
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
maxQueued?: number;
|
|
37
|
+
}): AsyncIterableIterator<WriteChange>;
|
|
38
|
+
/**
|
|
39
|
+
* An async iterator over the items a `register`ed listener receives,
|
|
40
|
+
* queued up to `maxQueued`; `register` returns the unregister call.
|
|
41
|
+
*/
|
|
42
|
+
queue<T>(register: (listener: (item: T) => void) => () => void, options: {
|
|
43
|
+
signal?: AbortSignal;
|
|
44
|
+
maxQueued?: number;
|
|
45
|
+
}): AsyncIterableIterator<T>;
|
|
46
|
+
/** Hand a committed mutation's write-set to the `onWrite` listeners. */
|
|
47
|
+
emit(change: WriteChange): void;
|
|
48
|
+
/** Stop the engine change feed (with `changeFeed: true`). */
|
|
49
|
+
close(): void;
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Statement } from '../driver.js';
|
|
2
|
+
/**
|
|
3
|
+
* A statement compiled once for a subscription, reused while its claims
|
|
4
|
+
* and the `$context` values it read are unchanged. `param` names the
|
|
5
|
+
* parameter each run fills in; `share` keys the reads subscribers share.
|
|
6
|
+
*/
|
|
7
|
+
export interface SubscriptionCompile<C> {
|
|
8
|
+
claims: string;
|
|
9
|
+
statistics: number;
|
|
10
|
+
variables: string | undefined;
|
|
11
|
+
contextReads: Array<[string, unknown]>;
|
|
12
|
+
compiled: C;
|
|
13
|
+
param: string;
|
|
14
|
+
share: string | undefined;
|
|
15
|
+
}
|
|
16
|
+
/** A cached compile still holds for these `$context` values. */
|
|
17
|
+
export declare function sameContextReads(reads: Array<[string, unknown]>, context: unknown): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* A compile made with `slot` as one parameter's value, reusable with that
|
|
20
|
+
* parameter filled in per run; undefined unless exactly one parameter
|
|
21
|
+
* holds the slot.
|
|
22
|
+
*/
|
|
23
|
+
export declare function slotCompile<C>(compiled: C, slot: unknown, claims: string | undefined, statistics: number, variables: string | undefined, contextReads: Array<[string, unknown]>, statement?: Statement): SubscriptionCompile<C> | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { WriteChange } from '../execute/changes.js';
|
|
2
|
+
import { NodeType } from '../model/types.js';
|
|
3
|
+
import { ChangeEvent } from '../schema/hooks.js';
|
|
4
|
+
/** The write an event came from, for reads shared across its subscribers. */
|
|
5
|
+
export declare const CHANGE: unique symbol;
|
|
6
|
+
/** On a CONNECT / DISCONNECT event: whether its node owns the field. */
|
|
7
|
+
export declare const SELF_OWNS: unique symbol;
|
|
8
|
+
/** A write's events for one node type, one per node, most specific first. */
|
|
9
|
+
export declare function changeEvents(change: WriteChange, node: NodeType): ChangeEvent[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GraphModel, NodeType } from '../model/types.js';
|
|
2
|
+
export interface FilterCostEnv {
|
|
3
|
+
model: GraphModel;
|
|
4
|
+
/** Measured maximum degrees, by `Owner.field` (see `analyze()`). */
|
|
5
|
+
degrees: ReadonlyMap<string, number>;
|
|
6
|
+
/** The `maxSubscriptionFilterDepth` option. */
|
|
7
|
+
maxDepth: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Estimated rows one changed node's `where` check touches: each
|
|
11
|
+
* relationship filter scans the relationship (its measured degree, else
|
|
12
|
+
* its cardinality, else a page of the target), times what it nests.
|
|
13
|
+
* Nesting deeper than `maxSubscriptionFilterDepth` is LIMIT_EXCEEDED.
|
|
14
|
+
*/
|
|
15
|
+
export declare function filterCost(env: FilterCostEnv, node: NodeType, where: Record<string, unknown> | null | undefined, depth: number): number;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { GraphQLResolveInfo, GraphQLSchema } from 'graphql';
|
|
2
|
+
import { CompileContext } from '../compile/context.js';
|
|
3
|
+
import { CompiledRead } from '../compile/read/types.js';
|
|
4
|
+
import { SelectionContext } from '../compile/selection.js';
|
|
5
|
+
import { LoraDriver, QueryResult, Statement } from '../driver.js';
|
|
6
|
+
import { WriteChange } from '../execute/changes.js';
|
|
7
|
+
import { GraphModel, NodeType } from '../model/types.js';
|
|
8
|
+
import { ChangeEvent } from '../schema/hooks.js';
|
|
9
|
+
import { ChangeHub } from './change-hub.js';
|
|
10
|
+
/**
|
|
11
|
+
* What subscriptions need of the `LoraGraphQL` instance that owns them:
|
|
12
|
+
* its limits, and the request machinery they share with queries.
|
|
13
|
+
*/
|
|
14
|
+
export interface SubscriptionHost {
|
|
15
|
+
model: GraphModel;
|
|
16
|
+
driver: LoraDriver;
|
|
17
|
+
changes: ChangeHub;
|
|
18
|
+
/** The `maxSubscriptions` option, defaulted. */
|
|
19
|
+
maxSubscriptions: number;
|
|
20
|
+
/** The `subscriptionScope` option, defaulted. */
|
|
21
|
+
subscriptionScope: (context: unknown) => object | undefined;
|
|
22
|
+
/** The `maxSubscriptionFilterDepth` option, defaulted. */
|
|
23
|
+
maxSubscriptionFilterDepth: number;
|
|
24
|
+
/** The `subscriptionTimeoutMs` option, defaulted. */
|
|
25
|
+
subscriptionTimeoutMs: number;
|
|
26
|
+
/** The executable schema. */
|
|
27
|
+
schema(): GraphQLSchema;
|
|
28
|
+
/** The request's verified claims. */
|
|
29
|
+
jwt(context: unknown): Record<string, unknown> | undefined;
|
|
30
|
+
/** A compile context for the request. */
|
|
31
|
+
context(base: SelectionContext, context: unknown): CompileContext;
|
|
32
|
+
/** Measured maximum degrees, by `Owner.field`. */
|
|
33
|
+
degrees(): ReadonlyMap<string, number>;
|
|
34
|
+
/** Bumped when statistics change. */
|
|
35
|
+
statisticsVersion(): number;
|
|
36
|
+
/** Charge `cost` to the operation; COST_EXCEEDED past its limit. */
|
|
37
|
+
charge(field: string, cost: number, context: unknown): void;
|
|
38
|
+
/** Run a compiled read as the root field `field`. */
|
|
39
|
+
run(field: string, compiled: CompiledRead, context: unknown, info?: GraphQLResolveInfo, change?: object, shareKey?: string): Promise<unknown>;
|
|
40
|
+
}
|
|
41
|
+
export declare class Subscriptions {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(host: SubscriptionHost);
|
|
44
|
+
/**
|
|
45
|
+
* Events for one node type, from the exact write-sets of mutations made
|
|
46
|
+
* through this instance. Nodes the subscriber may not read are skipped;
|
|
47
|
+
* deletions of rule-protected types are only sent to subscribers that
|
|
48
|
+
* follow that key and could read the node before it went (checked in
|
|
49
|
+
* the deleting transaction).
|
|
50
|
+
*/
|
|
51
|
+
subscribe(node: NodeType, args: Record<string, unknown>, context: unknown): AsyncIterableIterator<ChangeEvent>;
|
|
52
|
+
/**
|
|
53
|
+
* Inside a deleting transaction, before anything is deleted: which of
|
|
54
|
+
* the doomed nodes each subscriber following one of their keys may
|
|
55
|
+
* read, kept for the change's DELETE events. A node cannot be checked
|
|
56
|
+
* once it is gone, so a deletion nobody checked reaches nobody whose
|
|
57
|
+
* rules depend on the node.
|
|
58
|
+
*/
|
|
59
|
+
probeDeletes(change: WriteChange, doomed: ReadonlyArray<{
|
|
60
|
+
node: NodeType;
|
|
61
|
+
keys: unknown[];
|
|
62
|
+
}>, run: (statement: Statement) => Promise<QueryResult>): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* A changed node for a subscriber. The read is compiled once per
|
|
65
|
+
* subscription (context), field node, claims and variables, with the
|
|
66
|
+
* key as its only varying parameter.
|
|
67
|
+
*/
|
|
68
|
+
resolveByKey(node: NodeType, key: unknown, info: GraphQLResolveInfo, context: unknown, change?: object): Promise<unknown>;
|
|
69
|
+
/** `run` once per change for each distinct statement set. */
|
|
70
|
+
shared(change: object | undefined, statements: Statement[], run: () => Promise<QueryResult[]>,
|
|
71
|
+
/** Precomputed key of the statements, from a reused compile. */
|
|
72
|
+
shareKey?: string): Promise<QueryResult[]>;
|
|
73
|
+
}
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DocumentNode, ExecutionResult, GraphQLSchema } from 'graphql';
|
|
2
2
|
import { LoraDatabaseLike } from './driver.js';
|
|
3
|
-
import { LoraGraphQL
|
|
3
|
+
import { LoraGraphQL } from './lora-graphql.js';
|
|
4
|
+
import { LoraGraphQLOptions, StatementEvent } from './options.js';
|
|
4
5
|
/** The node binding's database, as far as tests use it. */
|
|
5
6
|
export type TestDatabase = LoraDatabaseLike & {
|
|
6
7
|
execute(query: string, params?: never): Promise<unknown>;
|
|
@@ -54,6 +55,8 @@ export interface AccessExpectations {
|
|
|
54
55
|
* - `connect Trip.members lou:tomorrowland → f1` (also `->`)
|
|
55
56
|
* - `disconnect Trip.members lou:tomorrowland → f1`
|
|
56
57
|
* - `update-edge Trip.members lou:tomorrowland → f1 {"rsvp": "GOING"}`
|
|
58
|
+
* (on a single relationship the edge is the current one, whichever
|
|
59
|
+
* node the entry names)
|
|
57
60
|
*
|
|
58
61
|
* Denied means `FORBIDDEN`, `UNAUTHENTICATED` or `NOT_FOUND` (a node the
|
|
59
62
|
* caller cannot see); any other error is reported as a mismatch either way.
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { graphql as g } from "graphql";
|
|
2
|
-
import { a as h, l as b } from "./driver-
|
|
2
|
+
import { a as h, l as b } from "./driver-CXKPLfkz.js";
|
|
3
3
|
async function T(l) {
|
|
4
4
|
let u;
|
|
5
5
|
try {
|
|
@@ -12,23 +12,23 @@ async function T(l) {
|
|
|
12
12
|
const i = await u(), e = [], { seed: t, onStatement: a, ...c } = l, n = new h({
|
|
13
13
|
...c,
|
|
14
14
|
driver: b(i),
|
|
15
|
-
onStatement: (
|
|
16
|
-
e.push(
|
|
15
|
+
onStatement: (s) => {
|
|
16
|
+
e.push(s), a?.(s);
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
if (await n.assertSchema({ create: !0 }), typeof t == "function") await t(i);
|
|
20
20
|
else
|
|
21
|
-
for (const
|
|
22
|
-
await i.execute(
|
|
23
|
-
const o = n.getSchema(),
|
|
21
|
+
for (const s of typeof t == "string" ? [t] : t ?? [])
|
|
22
|
+
await i.execute(s);
|
|
23
|
+
const o = n.getSchema(), r = (s, p = {}, d = {}) => g({ schema: o, source: s, variableValues: p, contextValue: d });
|
|
24
24
|
return {
|
|
25
25
|
lora: n,
|
|
26
26
|
db: i,
|
|
27
27
|
schema: o,
|
|
28
28
|
statements: e,
|
|
29
|
-
run:
|
|
30
|
-
data: async (
|
|
31
|
-
const $ = await s
|
|
29
|
+
run: r,
|
|
30
|
+
data: async (s, p, d) => {
|
|
31
|
+
const $ = await r(s, p, d);
|
|
32
32
|
if ($.errors) throw $.errors[0];
|
|
33
33
|
return $.data;
|
|
34
34
|
},
|
|
@@ -82,7 +82,7 @@ async function D(l, u, i) {
|
|
|
82
82
|
u
|
|
83
83
|
), o = await l.begin();
|
|
84
84
|
try {
|
|
85
|
-
const
|
|
85
|
+
const r = await l.execute({
|
|
86
86
|
source: e,
|
|
87
87
|
variables: t,
|
|
88
88
|
context: {
|
|
@@ -90,16 +90,16 @@ async function D(l, u, i) {
|
|
|
90
90
|
...i.as ? { jwt: i.as } : {},
|
|
91
91
|
transaction: o
|
|
92
92
|
}
|
|
93
|
-
}),
|
|
94
|
-
if (
|
|
95
|
-
const d =
|
|
96
|
-
return typeof d == "string" && v.has(d) ? { kind: "denied", message: `${d}: ${
|
|
93
|
+
}), s = r.errors?.[0];
|
|
94
|
+
if (s) {
|
|
95
|
+
const d = s.extensions?.code;
|
|
96
|
+
return typeof d == "string" && v.has(d) ? { kind: "denied", message: `${d}: ${s.message}` } : {
|
|
97
97
|
kind: "error",
|
|
98
|
-
message: `${String(d ?? "error")}: ${
|
|
98
|
+
message: `${String(d ?? "error")}: ${s.message}`
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
|
-
const p =
|
|
102
|
-
return n && (p == null || !n(p)) ? { kind: "denied", message: "not visible, nothing written" } : a &&
|
|
101
|
+
const p = r.data?.[c];
|
|
102
|
+
return n && (p == null || !n(p)) ? { kind: "denied", message: "not visible, nothing written" } : a && r.data?.[c] == null ? { kind: "denied", message: "not visible" } : { kind: "allowed" };
|
|
103
103
|
} finally {
|
|
104
104
|
await o.rollback();
|
|
105
105
|
}
|
|
@@ -112,9 +112,9 @@ function E(l, u) {
|
|
|
112
112
|
throw new Error(
|
|
113
113
|
"expected `<read|create|update|delete> Type key [json]` or `<connect|disconnect|update-edge> Type.field key → key [json]`"
|
|
114
114
|
);
|
|
115
|
-
const [, e, t, a, c, n, o] = i,
|
|
116
|
-
if (!
|
|
117
|
-
const
|
|
115
|
+
const [, e, t, a, c, n, o] = i, r = l.model.nodes.get(t);
|
|
116
|
+
if (!r) throw new Error(`${t} is not a @node type`);
|
|
117
|
+
const s = o ? JSON.parse(o) : {}, p = `${r.key.customScalar ?? r.key.type}!`, d = t.charAt(0).toLowerCase() + t.slice(1), $ = r.plural.charAt(0).toUpperCase() + r.plural.slice(1), w = ["connect", "disconnect", "update-edge"].includes(
|
|
118
118
|
e
|
|
119
119
|
);
|
|
120
120
|
if (w !== (a !== void 0 && n !== void 0))
|
|
@@ -125,7 +125,7 @@ function E(l, u) {
|
|
|
125
125
|
case "read":
|
|
126
126
|
return {
|
|
127
127
|
operation: e,
|
|
128
|
-
source: `query($key: ${p}) { ${d}(${
|
|
128
|
+
source: `query($key: ${p}) { ${d}(${r.key.name}: $key) { ${r.key.name} } }`,
|
|
129
129
|
variables: { key: c },
|
|
130
130
|
isRead: !0,
|
|
131
131
|
root: d
|
|
@@ -134,15 +134,15 @@ function E(l, u) {
|
|
|
134
134
|
return {
|
|
135
135
|
operation: e,
|
|
136
136
|
source: `mutation($input: [${t}CreateInput!]!) { create${$}(input: $input) { __typename } }`,
|
|
137
|
-
variables: { input: [{ ...
|
|
137
|
+
variables: { input: [{ ...s, [r.key.name]: c }] },
|
|
138
138
|
isRead: !1,
|
|
139
139
|
root: `create${$}`
|
|
140
140
|
};
|
|
141
141
|
case "update":
|
|
142
142
|
return {
|
|
143
143
|
operation: e,
|
|
144
|
-
source: `mutation($key: ${p}, $update: ${t}UpdateInput!) { update${t}(${
|
|
145
|
-
variables: { key: c, update:
|
|
144
|
+
source: `mutation($key: ${p}, $update: ${t}UpdateInput!) { update${t}(${r.key.name}: $key, update: $update) { ${d} { __typename } info { relationshipsDeleted } } }`,
|
|
145
|
+
variables: { key: c, update: s },
|
|
146
146
|
isRead: !1,
|
|
147
147
|
root: `update${t}`,
|
|
148
148
|
wrote: (f) => f[d] != null
|
|
@@ -150,22 +150,27 @@ function E(l, u) {
|
|
|
150
150
|
case "delete":
|
|
151
151
|
return {
|
|
152
152
|
operation: e,
|
|
153
|
-
source: `mutation($key: ${p}) { delete${t}(${
|
|
153
|
+
source: `mutation($key: ${p}) { delete${t}(${r.key.name}: $key) { nodesDeleted } }`,
|
|
154
154
|
variables: { key: c },
|
|
155
155
|
isRead: !1,
|
|
156
156
|
root: `delete${t}`,
|
|
157
157
|
wrote: (f) => Number(f.nodesDeleted ?? 0) > 0
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
|
-
const y =
|
|
160
|
+
const y = r.fields.get(a);
|
|
161
161
|
if (y?.kind !== "relationship")
|
|
162
162
|
throw new Error(`${t}.${a} is not a relationship field`);
|
|
163
163
|
const m = l.model.nodes.get(y.target)?.key.name ?? "key", k = e === "connect" ? {
|
|
164
|
-
connect: y.list ? [{ [m]: n, ...o ? { edge:
|
|
165
|
-
} : e === "disconnect" ? { disconnect: y.list ? [n] : !0 } :
|
|
164
|
+
connect: y.list ? [{ [m]: n, ...o ? { edge: s } : {} }] : { [m]: n, ...o ? { edge: s } : {} }
|
|
165
|
+
} : e === "disconnect" ? { disconnect: y.list ? [n] : !0 } : (
|
|
166
|
+
// A single relationship has one edge: the update names no key.
|
|
167
|
+
{
|
|
168
|
+
update: y.list ? [{ [m]: n, edge: s }] : { edge: s }
|
|
169
|
+
}
|
|
170
|
+
);
|
|
166
171
|
return {
|
|
167
172
|
operation: e,
|
|
168
|
-
source: `mutation($key: ${p}, $update: ${t}UpdateInput!) { update${t}(${
|
|
173
|
+
source: `mutation($key: ${p}, $update: ${t}UpdateInput!) { update${t}(${r.key.name}: $key, update: $update) { ${d} { __typename } info { relationshipsDeleted } } }`,
|
|
169
174
|
variables: { key: c, update: { [a]: k } },
|
|
170
175
|
isRead: !1,
|
|
171
176
|
root: `update${t}`,
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sources":["../src/testing.ts"],"sourcesContent":["// Test helpers: an in-memory LoraDB with the schema asserted, and a plan\n// assertion. Import from `@loradb/lora-graphql/testing`; needs\n// `@loradb/lora-node`. Framework-agnostic: failures throw plain errors.\n\nimport {\n graphql,\n type DocumentNode,\n type ExecutionResult,\n type GraphQLSchema,\n} from \"graphql\";\nimport { loraDriver, type LoraDatabaseLike } from \"./driver.js\";\nimport {\n LoraGraphQL,\n type LoraGraphQLOptions,\n type StatementEvent,\n} from \"./lora-graphql.js\";\n\n/** The node binding's database, as far as tests use it. */\nexport type TestDatabase = LoraDatabaseLike & {\n execute(query: string, params?: never): Promise<unknown>;\n dispose?(): void;\n};\n\nexport interface TestLoraGraphQLOptions extends Omit<\n LoraGraphQLOptions,\n \"driver\"\n> {\n /** Cypher to run after the schema is asserted, or a function given the database. */\n seed?: string | string[] | ((db: TestDatabase) => Promise<void>);\n}\n\nexport interface TestLoraGraphQL {\n lora: LoraGraphQL;\n db: TestDatabase;\n schema: GraphQLSchema;\n /** Every statement run so far (clear with `statements.length = 0`). */\n statements: StatementEvent[];\n /** Execute a document; errors stay in the result. */\n run: (\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<ExecutionResult>;\n /** Execute a document and return its data; throws on the first error. */\n data: <T = Record<string, unknown>>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<T>;\n /** Release the database. */\n close: () => void;\n}\n\n/** A LoraGraphQL over a fresh in-memory database, schema asserted and seeded. */\nexport async function createTestLoraGraphQL(\n options: TestLoraGraphQLOptions,\n): Promise<TestLoraGraphQL> {\n let createDatabase: () => Promise<TestDatabase>;\n try {\n ({ createDatabase } = (await import(\"@loradb/lora-node\")) as unknown as {\n createDatabase: typeof createDatabase;\n });\n } catch {\n throw new Error(\n \"createTestLoraGraphQL needs @loradb/lora-node: npm install --save-dev @loradb/lora-node\",\n );\n }\n const db = await createDatabase();\n const statements: StatementEvent[] = [];\n const { seed, onStatement, ...rest } = options;\n const lora = new LoraGraphQL({\n ...rest,\n driver: loraDriver(db),\n onStatement: (event) => {\n statements.push(event);\n onStatement?.(event);\n },\n });\n await lora.assertSchema({ create: true });\n if (typeof seed === \"function\") await seed(db);\n else {\n for (const text of typeof seed === \"string\" ? [seed] : (seed ?? [])) {\n await db.execute(text);\n }\n }\n const schema = lora.getSchema();\n const run = (\n source: string,\n variables: Record<string, unknown> = {},\n contextValue: Record<string, unknown> = {},\n ) => graphql({ schema, source, variableValues: variables, contextValue });\n return {\n lora,\n db,\n schema,\n statements,\n run,\n data: async <T>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => {\n const r = await run(source, variables, context);\n if (r.errors) throw r.errors[0];\n return r.data as T;\n },\n close: () => db.dispose?.(),\n };\n}\n\n/**\n * Assert that every statement of a query's root fields uses the index\n * access it was compiled for (no label scans where a seek was expected,\n * no mutating plans, no row-budget excess). Throws listing each finding.\n */\nexport async function expectSeeks(\n lora: LoraGraphQL,\n document: string | DocumentNode,\n variables: Record<string, unknown> = {},\n options: { rowBudget?: number; context?: unknown } = {},\n): Promise<void> {\n const fields = await lora.explain(document, variables, options);\n const findings = fields.flatMap((f) =>\n f.reports.flatMap((r) =>\n r.findings.map((x) => `${f.field}: ${x.message}\\n ${x.statement}`),\n ),\n );\n if (findings.length > 0) {\n throw new Error(\n `expected every root field to seek:\\n${findings.join(\"\\n\")}`,\n );\n }\n}\n\n/** The access `expectAccess` expects: entries allowed, and entries denied. */\nexport interface AccessExpectations {\n /** The claims to act as; undefined acts anonymously. */\n as: Record<string, unknown> | undefined;\n /** Other GraphQL context values the rules read (`$context.*`). */\n context?: Record<string, unknown>;\n allowed?: readonly string[];\n denied?: readonly string[];\n}\n\n/**\n * Assert who may do what, against the database: each entry runs as the\n * caller in a transaction that is always rolled back, and every mismatch\n * is reported at once. Entries:\n *\n * - `read Trip lou:tomorrowland` (denied: not visible, or refused)\n * - `create Trip lou:x {\"name\": \"X\"}` (other input fields as JSON)\n * - `update Trip lou:x {\"name\": \"Y\"}` and `delete Trip lou:x`\n * - `connect Trip.members lou:tomorrowland → f1` (also `->`)\n * - `disconnect Trip.members lou:tomorrowland → f1`\n * - `update-edge Trip.members lou:tomorrowland → f1 {\"rsvp\": \"GOING\"}`\n *\n * Denied means `FORBIDDEN`, `UNAUTHENTICATED` or `NOT_FOUND` (a node the\n * caller cannot see); any other error is reported as a mismatch either way.\n */\nexport async function expectAccess(\n target: LoraGraphQL | { lora: LoraGraphQL },\n expectations: AccessExpectations,\n): Promise<void> {\n const lora = target instanceof LoraGraphQL ? target : target.lora;\n const problems: string[] = [];\n const check = async (entry: string, allowed: boolean) => {\n let outcome: AccessOutcome;\n try {\n outcome = await tryAccess(lora, entry, expectations);\n } catch (err) {\n problems.push(\n `${entry}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return;\n }\n if (outcome.kind === \"error\") {\n problems.push(`${entry}: failed with ${outcome.message}`);\n } else if (allowed && outcome.kind === \"denied\") {\n problems.push(\n `expected allowed, was denied: ${entry} (${outcome.message})`,\n );\n } else if (!allowed && outcome.kind === \"allowed\") {\n problems.push(`expected denied, was allowed: ${entry}`);\n }\n };\n for (const entry of expectations.allowed ?? []) await check(entry, true);\n for (const entry of expectations.denied ?? []) await check(entry, false);\n if (problems.length > 0) {\n const who = expectations.as ? JSON.stringify(expectations.as) : \"anonymous\";\n throw new Error(\n `access as ${who} differs in ${problems.length} ${problems.length === 1 ? \"entry\" : \"entries\"}:\\n ${problems.join(\"\\n \")}`,\n );\n }\n}\n\ntype AccessOutcome =\n | { kind: \"allowed\" }\n | { kind: \"denied\"; message: string }\n | { kind: \"error\"; message: string };\n\nconst DENIED = new Set([\"FORBIDDEN\", \"UNAUTHENTICATED\", \"NOT_FOUND\"]);\n\nasync function tryAccess(\n lora: LoraGraphQL,\n entry: string,\n expectations: AccessExpectations,\n): Promise<AccessOutcome> {\n const { source, variables, isRead, root, wrote } = accessOperation(\n lora,\n entry,\n );\n const tx = await lora.begin();\n try {\n const result = await lora.execute({\n source,\n variables,\n context: {\n ...expectations.context,\n ...(expectations.as ? { jwt: expectations.as } : {}),\n transaction: tx,\n },\n });\n const error = result.errors?.[0];\n if (error) {\n const code = (error.extensions as Record<string, unknown> | undefined)?.[\n \"code\"\n ];\n return typeof code === \"string\" && DENIED.has(code)\n ? { kind: \"denied\", message: `${code}: ${error.message}` }\n : {\n kind: \"error\",\n message: `${String(code ?? \"error\")}: ${error.message}`,\n };\n }\n const payload = (result.data as Record<string, unknown> | null)?.[root];\n if (\n wrote &&\n (payload == null || !wrote(payload as Record<string, unknown>))\n ) {\n return { kind: \"denied\", message: \"not visible, nothing written\" };\n }\n if (isRead) {\n const value = (result.data as Record<string, unknown> | null)?.[root];\n if (value == null) return { kind: \"denied\", message: \"not visible\" };\n }\n return { kind: \"allowed\" };\n } finally {\n await tx.rollback();\n }\n}\n\nfunction accessOperation(\n lora: LoraGraphQL,\n entry: string,\n): {\n operation: string;\n source: string;\n variables: Record<string, unknown>;\n isRead: boolean;\n root: string;\n /** How the payload says nothing was written: a hidden target answers no error. */\n wrote?: (payload: Record<string, unknown>) => boolean;\n} {\n const m =\n /^\\s*(read|create|update|delete|connect|disconnect|update-edge)\\s+([A-Za-z_][\\w]*)(?:\\.([A-Za-z_]\\w*))?\\s+(\\S+)(?:\\s*(?:→|->)\\s*(\\S+))?\\s*(\\{.*\\})?\\s*$/s.exec(\n entry,\n );\n if (!m) {\n throw new Error(\n \"expected `<read|create|update|delete> Type key [json]` or `<connect|disconnect|update-edge> Type.field key → key [json]`\",\n );\n }\n const [, operation, typeName, fieldName, key, other, json] = m as unknown as [\n string,\n string,\n string,\n string | undefined,\n string,\n string | undefined,\n string | undefined,\n ];\n const node = lora.model.nodes.get(typeName);\n if (!node) throw new Error(`${typeName} is not a @node type`);\n const input = json ? (JSON.parse(json) as Record<string, unknown>) : {};\n const keyType = `${node.key.customScalar ?? node.key.type}!`;\n const lower = typeName.charAt(0).toLowerCase() + typeName.slice(1);\n const upperPlural =\n node.plural.charAt(0).toUpperCase() + node.plural.slice(1);\n const relational = [\"connect\", \"disconnect\", \"update-edge\"].includes(\n operation,\n );\n if (relational !== (fieldName !== undefined && other !== undefined)) {\n throw new Error(\n relational\n ? `${operation} takes Type.field key → key`\n : `${operation} takes Type key`,\n );\n }\n switch (operation) {\n case \"read\":\n return {\n operation,\n source: `query($key: ${keyType}) { ${lower}(${node.key.name}: $key) { ${node.key.name} } }`,\n variables: { key },\n isRead: true,\n root: lower,\n };\n case \"create\":\n return {\n operation,\n source: `mutation($input: [${typeName}CreateInput!]!) { create${upperPlural}(input: $input) { __typename } }`,\n variables: { input: [{ ...input, [node.key.name]: key }] },\n isRead: false,\n root: `create${upperPlural}`,\n };\n case \"update\":\n return {\n operation,\n source: `mutation($key: ${keyType}, $update: ${typeName}UpdateInput!) { update${typeName}(${node.key.name}: $key, update: $update) { ${lower} { __typename } info { relationshipsDeleted } } }`,\n variables: { key, update: input },\n isRead: false,\n root: `update${typeName}`,\n wrote: (p) => p[lower] != null,\n };\n case \"delete\":\n return {\n operation,\n source: `mutation($key: ${keyType}) { delete${typeName}(${node.key.name}: $key) { nodesDeleted } }`,\n variables: { key },\n isRead: false,\n root: `delete${typeName}`,\n wrote: (p) => Number(p[\"nodesDeleted\"] ?? 0) > 0,\n };\n }\n const field = node.fields.get(fieldName!);\n if (field?.kind !== \"relationship\") {\n throw new Error(`${typeName}.${fieldName} is not a relationship field`);\n }\n const targetKey = lora.model.nodes.get(field.target)?.key.name ?? \"key\";\n const change =\n operation === \"connect\"\n ? {\n connect: field.list\n ? [{ [targetKey]: other, ...(json ? { edge: input } : {}) }]\n : { [targetKey]: other, ...(json ? { edge: input } : {}) },\n }\n : operation === \"disconnect\"\n ? { disconnect: field.list ? [other] : true }\n : { update: [{ [targetKey]: other, edge: input }] };\n return {\n operation,\n source: `mutation($key: ${keyType}, $update: ${typeName}UpdateInput!) { update${typeName}(${node.key.name}: $key, update: $update) { ${lower} { __typename } info { relationshipsDeleted } } }`,\n variables: { key, update: { [fieldName!]: change } },\n isRead: false,\n root: `update${typeName}`,\n // A hidden owner answers a null node; a disconnect of a pair the\n // caller may not touch removes nothing.\n wrote: (p) =>\n p[lower] != null &&\n (operation !== \"disconnect\" ||\n Number(\n (p[\"info\"] as Record<string, unknown> | undefined)?.[\n \"relationshipsDeleted\"\n ] ?? 0,\n ) > 0),\n };\n}\n"],"names":["createTestLoraGraphQL","options","createDatabase","db","statements","seed","onStatement","rest","lora","LoraGraphQL","loraDriver","event","text","schema","run","source","variables","contextValue","graphql","context","r","expectSeeks","document","findings","f","x","expectAccess","target","expectations","problems","check","entry","allowed","outcome","tryAccess","err","who","DENIED","isRead","root","wrote","accessOperation","tx","result","error","code","payload","m","operation","typeName","fieldName","key","other","json","node","input","keyType","lower","upperPlural","relational","p","field","targetKey","change"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"testing.js","sources":["../src/testing.ts"],"sourcesContent":["// Test helpers: an in-memory LoraDB with the schema asserted, and a plan\n// assertion. Import from `@loradb/lora-graphql/testing`; needs\n// `@loradb/lora-node`. Framework-agnostic: failures throw plain errors.\n\nimport {\n graphql,\n type DocumentNode,\n type ExecutionResult,\n type GraphQLSchema,\n} from \"graphql\";\nimport { loraDriver, type LoraDatabaseLike } from \"./driver.js\";\nimport { LoraGraphQL } from \"./lora-graphql.js\";\nimport { type LoraGraphQLOptions, type StatementEvent } from \"./options.js\";\n\n/** The node binding's database, as far as tests use it. */\nexport type TestDatabase = LoraDatabaseLike & {\n execute(query: string, params?: never): Promise<unknown>;\n dispose?(): void;\n};\n\nexport interface TestLoraGraphQLOptions extends Omit<\n LoraGraphQLOptions,\n \"driver\"\n> {\n /** Cypher to run after the schema is asserted, or a function given the database. */\n seed?: string | string[] | ((db: TestDatabase) => Promise<void>);\n}\n\nexport interface TestLoraGraphQL {\n lora: LoraGraphQL;\n db: TestDatabase;\n schema: GraphQLSchema;\n /** Every statement run so far (clear with `statements.length = 0`). */\n statements: StatementEvent[];\n /** Execute a document; errors stay in the result. */\n run: (\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<ExecutionResult>;\n /** Execute a document and return its data; throws on the first error. */\n data: <T = Record<string, unknown>>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => Promise<T>;\n /** Release the database. */\n close: () => void;\n}\n\n/** A LoraGraphQL over a fresh in-memory database, schema asserted and seeded. */\nexport async function createTestLoraGraphQL(\n options: TestLoraGraphQLOptions,\n): Promise<TestLoraGraphQL> {\n let createDatabase: () => Promise<TestDatabase>;\n try {\n ({ createDatabase } = (await import(\"@loradb/lora-node\")) as unknown as {\n createDatabase: typeof createDatabase;\n });\n } catch {\n throw new Error(\n \"createTestLoraGraphQL needs @loradb/lora-node: npm install --save-dev @loradb/lora-node\",\n );\n }\n const db = await createDatabase();\n const statements: StatementEvent[] = [];\n const { seed, onStatement, ...rest } = options;\n const lora = new LoraGraphQL({\n ...rest,\n driver: loraDriver(db),\n onStatement: (event) => {\n statements.push(event);\n onStatement?.(event);\n },\n });\n await lora.assertSchema({ create: true });\n if (typeof seed === \"function\") await seed(db);\n else {\n for (const text of typeof seed === \"string\" ? [seed] : (seed ?? [])) {\n await db.execute(text);\n }\n }\n const schema = lora.getSchema();\n const run = (\n source: string,\n variables: Record<string, unknown> = {},\n contextValue: Record<string, unknown> = {},\n ) => graphql({ schema, source, variableValues: variables, contextValue });\n return {\n lora,\n db,\n schema,\n statements,\n run,\n data: async <T>(\n source: string,\n variables?: Record<string, unknown>,\n context?: Record<string, unknown>,\n ) => {\n const r = await run(source, variables, context);\n if (r.errors) throw r.errors[0];\n return r.data as T;\n },\n close: () => db.dispose?.(),\n };\n}\n\n/**\n * Assert that every statement of a query's root fields uses the index\n * access it was compiled for (no label scans where a seek was expected,\n * no mutating plans, no row-budget excess). Throws listing each finding.\n */\nexport async function expectSeeks(\n lora: LoraGraphQL,\n document: string | DocumentNode,\n variables: Record<string, unknown> = {},\n options: { rowBudget?: number; context?: unknown } = {},\n): Promise<void> {\n const fields = await lora.explain(document, variables, options);\n const findings = fields.flatMap((f) =>\n f.reports.flatMap((r) =>\n r.findings.map((x) => `${f.field}: ${x.message}\\n ${x.statement}`),\n ),\n );\n if (findings.length > 0) {\n throw new Error(\n `expected every root field to seek:\\n${findings.join(\"\\n\")}`,\n );\n }\n}\n\n/** The access `expectAccess` expects: entries allowed, and entries denied. */\nexport interface AccessExpectations {\n /** The claims to act as; undefined acts anonymously. */\n as: Record<string, unknown> | undefined;\n /** Other GraphQL context values the rules read (`$context.*`). */\n context?: Record<string, unknown>;\n allowed?: readonly string[];\n denied?: readonly string[];\n}\n\n/**\n * Assert who may do what, against the database: each entry runs as the\n * caller in a transaction that is always rolled back, and every mismatch\n * is reported at once. Entries:\n *\n * - `read Trip lou:tomorrowland` (denied: not visible, or refused)\n * - `create Trip lou:x {\"name\": \"X\"}` (other input fields as JSON)\n * - `update Trip lou:x {\"name\": \"Y\"}` and `delete Trip lou:x`\n * - `connect Trip.members lou:tomorrowland → f1` (also `->`)\n * - `disconnect Trip.members lou:tomorrowland → f1`\n * - `update-edge Trip.members lou:tomorrowland → f1 {\"rsvp\": \"GOING\"}`\n * (on a single relationship the edge is the current one, whichever\n * node the entry names)\n *\n * Denied means `FORBIDDEN`, `UNAUTHENTICATED` or `NOT_FOUND` (a node the\n * caller cannot see); any other error is reported as a mismatch either way.\n */\nexport async function expectAccess(\n target: LoraGraphQL | { lora: LoraGraphQL },\n expectations: AccessExpectations,\n): Promise<void> {\n const lora = target instanceof LoraGraphQL ? target : target.lora;\n const problems: string[] = [];\n const check = async (entry: string, allowed: boolean) => {\n let outcome: AccessOutcome;\n try {\n outcome = await tryAccess(lora, entry, expectations);\n } catch (err) {\n problems.push(\n `${entry}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return;\n }\n if (outcome.kind === \"error\") {\n problems.push(`${entry}: failed with ${outcome.message}`);\n } else if (allowed && outcome.kind === \"denied\") {\n problems.push(\n `expected allowed, was denied: ${entry} (${outcome.message})`,\n );\n } else if (!allowed && outcome.kind === \"allowed\") {\n problems.push(`expected denied, was allowed: ${entry}`);\n }\n };\n for (const entry of expectations.allowed ?? []) await check(entry, true);\n for (const entry of expectations.denied ?? []) await check(entry, false);\n if (problems.length > 0) {\n const who = expectations.as ? JSON.stringify(expectations.as) : \"anonymous\";\n throw new Error(\n `access as ${who} differs in ${problems.length} ${problems.length === 1 ? \"entry\" : \"entries\"}:\\n ${problems.join(\"\\n \")}`,\n );\n }\n}\n\ntype AccessOutcome =\n | { kind: \"allowed\" }\n | { kind: \"denied\"; message: string }\n | { kind: \"error\"; message: string };\n\nconst DENIED = new Set([\"FORBIDDEN\", \"UNAUTHENTICATED\", \"NOT_FOUND\"]);\n\nasync function tryAccess(\n lora: LoraGraphQL,\n entry: string,\n expectations: AccessExpectations,\n): Promise<AccessOutcome> {\n const { source, variables, isRead, root, wrote } = accessOperation(\n lora,\n entry,\n );\n const tx = await lora.begin();\n try {\n const result = await lora.execute({\n source,\n variables,\n context: {\n ...expectations.context,\n ...(expectations.as ? { jwt: expectations.as } : {}),\n transaction: tx,\n },\n });\n const error = result.errors?.[0];\n if (error) {\n const code = (error.extensions as Record<string, unknown> | undefined)?.[\n \"code\"\n ];\n return typeof code === \"string\" && DENIED.has(code)\n ? { kind: \"denied\", message: `${code}: ${error.message}` }\n : {\n kind: \"error\",\n message: `${String(code ?? \"error\")}: ${error.message}`,\n };\n }\n const payload = (result.data as Record<string, unknown> | null)?.[root];\n if (\n wrote &&\n (payload == null || !wrote(payload as Record<string, unknown>))\n ) {\n return { kind: \"denied\", message: \"not visible, nothing written\" };\n }\n if (isRead) {\n const value = (result.data as Record<string, unknown> | null)?.[root];\n if (value == null) return { kind: \"denied\", message: \"not visible\" };\n }\n return { kind: \"allowed\" };\n } finally {\n await tx.rollback();\n }\n}\n\nfunction accessOperation(\n lora: LoraGraphQL,\n entry: string,\n): {\n operation: string;\n source: string;\n variables: Record<string, unknown>;\n isRead: boolean;\n root: string;\n /** How the payload says nothing was written: a hidden target answers no error. */\n wrote?: (payload: Record<string, unknown>) => boolean;\n} {\n const m =\n /^\\s*(read|create|update|delete|connect|disconnect|update-edge)\\s+([A-Za-z_][\\w]*)(?:\\.([A-Za-z_]\\w*))?\\s+(\\S+)(?:\\s*(?:→|->)\\s*(\\S+))?\\s*(\\{.*\\})?\\s*$/s.exec(\n entry,\n );\n if (!m) {\n throw new Error(\n \"expected `<read|create|update|delete> Type key [json]` or `<connect|disconnect|update-edge> Type.field key → key [json]`\",\n );\n }\n const [, operation, typeName, fieldName, key, other, json] = m as unknown as [\n string,\n string,\n string,\n string | undefined,\n string,\n string | undefined,\n string | undefined,\n ];\n const node = lora.model.nodes.get(typeName);\n if (!node) throw new Error(`${typeName} is not a @node type`);\n const input = json ? (JSON.parse(json) as Record<string, unknown>) : {};\n const keyType = `${node.key.customScalar ?? node.key.type}!`;\n const lower = typeName.charAt(0).toLowerCase() + typeName.slice(1);\n const upperPlural =\n node.plural.charAt(0).toUpperCase() + node.plural.slice(1);\n const relational = [\"connect\", \"disconnect\", \"update-edge\"].includes(\n operation,\n );\n if (relational !== (fieldName !== undefined && other !== undefined)) {\n throw new Error(\n relational\n ? `${operation} takes Type.field key → key`\n : `${operation} takes Type key`,\n );\n }\n switch (operation) {\n case \"read\":\n return {\n operation,\n source: `query($key: ${keyType}) { ${lower}(${node.key.name}: $key) { ${node.key.name} } }`,\n variables: { key },\n isRead: true,\n root: lower,\n };\n case \"create\":\n return {\n operation,\n source: `mutation($input: [${typeName}CreateInput!]!) { create${upperPlural}(input: $input) { __typename } }`,\n variables: { input: [{ ...input, [node.key.name]: key }] },\n isRead: false,\n root: `create${upperPlural}`,\n };\n case \"update\":\n return {\n operation,\n source: `mutation($key: ${keyType}, $update: ${typeName}UpdateInput!) { update${typeName}(${node.key.name}: $key, update: $update) { ${lower} { __typename } info { relationshipsDeleted } } }`,\n variables: { key, update: input },\n isRead: false,\n root: `update${typeName}`,\n wrote: (p) => p[lower] != null,\n };\n case \"delete\":\n return {\n operation,\n source: `mutation($key: ${keyType}) { delete${typeName}(${node.key.name}: $key) { nodesDeleted } }`,\n variables: { key },\n isRead: false,\n root: `delete${typeName}`,\n wrote: (p) => Number(p[\"nodesDeleted\"] ?? 0) > 0,\n };\n }\n const field = node.fields.get(fieldName!);\n if (field?.kind !== \"relationship\") {\n throw new Error(`${typeName}.${fieldName} is not a relationship field`);\n }\n const targetKey = lora.model.nodes.get(field.target)?.key.name ?? \"key\";\n const change =\n operation === \"connect\"\n ? {\n connect: field.list\n ? [{ [targetKey]: other, ...(json ? { edge: input } : {}) }]\n : { [targetKey]: other, ...(json ? { edge: input } : {}) },\n }\n : operation === \"disconnect\"\n ? { disconnect: field.list ? [other] : true }\n : // A single relationship has one edge: the update names no key.\n {\n update: field.list\n ? [{ [targetKey]: other, edge: input }]\n : { edge: input },\n };\n return {\n operation,\n source: `mutation($key: ${keyType}, $update: ${typeName}UpdateInput!) { update${typeName}(${node.key.name}: $key, update: $update) { ${lower} { __typename } info { relationshipsDeleted } } }`,\n variables: { key, update: { [fieldName!]: change } },\n isRead: false,\n root: `update${typeName}`,\n // A hidden owner answers a null node; a disconnect of a pair the\n // caller may not touch removes nothing.\n wrote: (p) =>\n p[lower] != null &&\n (operation !== \"disconnect\" ||\n Number(\n (p[\"info\"] as Record<string, unknown> | undefined)?.[\n \"relationshipsDeleted\"\n ] ?? 0,\n ) > 0),\n };\n}\n"],"names":["createTestLoraGraphQL","options","createDatabase","db","statements","seed","onStatement","rest","lora","LoraGraphQL","loraDriver","event","text","schema","run","source","variables","contextValue","graphql","context","r","expectSeeks","document","findings","f","x","expectAccess","target","expectations","problems","check","entry","allowed","outcome","tryAccess","err","who","DENIED","isRead","root","wrote","accessOperation","tx","result","error","code","payload","m","operation","typeName","fieldName","key","other","json","node","input","keyType","lower","upperPlural","relational","p","field","targetKey","change"],"mappings":";;AAmDA,eAAsBA,EACpBC,GAC0B;AAC1B,MAAIC;AACJ,MAAI;AACF,KAAC,EAAE,gBAAAA,EAAA,IAAoB,MAAM,OAAO,mBAAmB;AAAA,EAGzD,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AACA,QAAMC,IAAK,MAAMD,EAAA,GACXE,IAA+B,CAAA,GAC/B,EAAE,MAAAC,GAAM,aAAAC,GAAa,GAAGC,MAASN,GACjCO,IAAO,IAAIC,EAAY;AAAA,IAC3B,GAAGF;AAAA,IACH,QAAQG,EAAWP,CAAE;AAAA,IACrB,aAAa,CAACQ,MAAU;AACtB,MAAAP,EAAW,KAAKO,CAAK,GACrBL,IAAcK,CAAK;AAAA,IACrB;AAAA,EAAA,CACD;AAED,MADA,MAAMH,EAAK,aAAa,EAAE,QAAQ,IAAM,GACpC,OAAOH,KAAS,WAAY,OAAMA,EAAKF,CAAE;AAAA;AAE3C,eAAWS,KAAQ,OAAOP,KAAS,WAAW,CAACA,CAAI,IAAKA,KAAQ;AAC9D,YAAMF,EAAG,QAAQS,CAAI;AAGzB,QAAMC,IAASL,EAAK,UAAA,GACdM,IAAM,CACVC,GACAC,IAAqC,CAAA,GACrCC,IAAwC,CAAA,MACrCC,EAAQ,EAAE,QAAAL,GAAQ,QAAAE,GAAQ,gBAAgBC,GAAW,cAAAC,GAAc;AACxE,SAAO;AAAA,IACL,MAAAT;AAAA,IACA,IAAAL;AAAA,IACA,QAAAU;AAAA,IACA,YAAAT;AAAA,IACA,KAAAU;AAAA,IACA,MAAM,OACJC,GACAC,GACAG,MACG;AACH,YAAMC,IAAI,MAAMN,EAAIC,GAAQC,GAAWG,CAAO;AAC9C,UAAIC,EAAE,OAAQ,OAAMA,EAAE,OAAO,CAAC;AAC9B,aAAOA,EAAE;AAAA,IACX;AAAA,IACA,OAAO,MAAMjB,EAAG,UAAA;AAAA,EAAU;AAE9B;AAOA,eAAsBkB,EACpBb,GACAc,GACAN,IAAqC,CAAA,GACrCf,IAAqD,IACtC;AAEf,QAAMsB,KADS,MAAMf,EAAK,QAAQc,GAAUN,GAAWf,CAAO,GACtC;AAAA,IAAQ,CAACuB,MAC/BA,EAAE,QAAQ;AAAA,MAAQ,CAACJ,MACjBA,EAAE,SAAS,IAAI,CAACK,MAAM,GAAGD,EAAE,KAAK,KAAKC,EAAE,OAAO;AAAA,IAAOA,EAAE,SAAS,EAAE;AAAA,IAAA;AAAA,EACpE;AAEF,MAAIF,EAAS,SAAS;AACpB,UAAM,IAAI;AAAA,MACR;AAAA,EAAuCA,EAAS,KAAK;AAAA,CAAI,CAAC;AAAA,IAAA;AAGhE;AA6BA,eAAsBG,EACpBC,GACAC,GACe;AACf,QAAMpB,IAAOmB,aAAkBlB,IAAckB,IAASA,EAAO,MACvDE,IAAqB,CAAA,GACrBC,IAAQ,OAAOC,GAAeC,MAAqB;AACvD,QAAIC;AACJ,QAAI;AACF,MAAAA,IAAU,MAAMC,EAAU1B,GAAMuB,GAAOH,CAAY;AAAA,IACrD,SAASO,GAAK;AACZ,MAAAN,EAAS;AAAA,QACP,GAAGE,CAAK,KAAKI,aAAe,QAAQA,EAAI,UAAU,OAAOA,CAAG,CAAC;AAAA,MAAA;AAE/D;AAAA,IACF;AACA,IAAIF,EAAQ,SAAS,UACnBJ,EAAS,KAAK,GAAGE,CAAK,iBAAiBE,EAAQ,OAAO,EAAE,IAC/CD,KAAWC,EAAQ,SAAS,WACrCJ,EAAS;AAAA,MACP,iCAAiCE,CAAK,KAAKE,EAAQ,OAAO;AAAA,IAAA,IAEnD,CAACD,KAAWC,EAAQ,SAAS,aACtCJ,EAAS,KAAK,iCAAiCE,CAAK,EAAE;AAAA,EAE1D;AACA,aAAWA,KAASH,EAAa,WAAW,CAAA,EAAI,OAAME,EAAMC,GAAO,EAAI;AACvE,aAAWA,KAASH,EAAa,UAAU,CAAA,EAAI,OAAME,EAAMC,GAAO,EAAK;AACvE,MAAIF,EAAS,SAAS,GAAG;AACvB,UAAMO,IAAMR,EAAa,KAAK,KAAK,UAAUA,EAAa,EAAE,IAAI;AAChE,UAAM,IAAI;AAAA,MACR,aAAaQ,CAAG,eAAeP,EAAS,MAAM,IAAIA,EAAS,WAAW,IAAI,UAAU,SAAS;AAAA,IAAQA,EAAS,KAAK;AAAA,GAAM,CAAC;AAAA,IAAA;AAAA,EAE9H;AACF;AAOA,MAAMQ,IAAS,oBAAI,IAAI,CAAC,aAAa,mBAAmB,WAAW,CAAC;AAEpE,eAAeH,EACb1B,GACAuB,GACAH,GACwB;AACxB,QAAM,EAAE,QAAAb,GAAQ,WAAAC,GAAW,QAAAsB,GAAQ,MAAAC,GAAM,OAAAC,MAAUC;AAAA,IACjDjC;AAAA,IACAuB;AAAA,EAAA,GAEIW,IAAK,MAAMlC,EAAK,MAAA;AACtB,MAAI;AACF,UAAMmC,IAAS,MAAMnC,EAAK,QAAQ;AAAA,MAChC,QAAAO;AAAA,MACA,WAAAC;AAAA,MACA,SAAS;AAAA,QACP,GAAGY,EAAa;AAAA,QAChB,GAAIA,EAAa,KAAK,EAAE,KAAKA,EAAa,GAAA,IAAO,CAAA;AAAA,QACjD,aAAac;AAAA,MAAA;AAAA,IACf,CACD,GACKE,IAAQD,EAAO,SAAS,CAAC;AAC/B,QAAIC,GAAO;AACT,YAAMC,IAAQD,EAAM,YAClB;AAEF,aAAO,OAAOC,KAAS,YAAYR,EAAO,IAAIQ,CAAI,IAC9C,EAAE,MAAM,UAAU,SAAS,GAAGA,CAAI,KAAKD,EAAM,OAAO,OACpD;AAAA,QACE,MAAM;AAAA,QACN,SAAS,GAAG,OAAOC,KAAQ,OAAO,CAAC,KAAKD,EAAM,OAAO;AAAA,MAAA;AAAA,IAE7D;AACA,UAAME,IAAWH,EAAO,OAA0CJ,CAAI;AACtE,WACEC,MACCM,KAAW,QAAQ,CAACN,EAAMM,CAAkC,KAEtD,EAAE,MAAM,UAAU,SAAS,+BAAA,IAEhCR,KACaK,EAAO,OAA0CJ,CAAI,KACvD,OAAa,EAAE,MAAM,UAAU,SAAS,cAAA,IAEhD,EAAE,MAAM,UAAA;AAAA,EACjB,UAAA;AACE,UAAMG,EAAG,SAAA;AAAA,EACX;AACF;AAEA,SAASD,EACPjC,GACAuB,GASA;AACA,QAAMgB,IACJ,0JAA0J;AAAA,IACxJhB;AAAA,EAAA;AAEJ,MAAI,CAACgB;AACH,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAGJ,QAAM,CAAA,EAAGC,GAAWC,GAAUC,GAAWC,GAAKC,GAAOC,CAAI,IAAIN,GASvDO,IAAO9C,EAAK,MAAM,MAAM,IAAIyC,CAAQ;AAC1C,MAAI,CAACK,EAAM,OAAM,IAAI,MAAM,GAAGL,CAAQ,sBAAsB;AAC5D,QAAMM,IAAQF,IAAQ,KAAK,MAAMA,CAAI,IAAgC,CAAA,GAC/DG,IAAU,GAAGF,EAAK,IAAI,gBAAgBA,EAAK,IAAI,IAAI,KACnDG,IAAQR,EAAS,OAAO,CAAC,EAAE,gBAAgBA,EAAS,MAAM,CAAC,GAC3DS,IACJJ,EAAK,OAAO,OAAO,CAAC,EAAE,YAAA,IAAgBA,EAAK,OAAO,MAAM,CAAC,GACrDK,IAAa,CAAC,WAAW,cAAc,aAAa,EAAE;AAAA,IAC1DX;AAAA,EAAA;AAEF,MAAIW,OAAgBT,MAAc,UAAaE,MAAU;AACvD,UAAM,IAAI;AAAA,MACRO,IACI,GAAGX,CAAS,gCACZ,GAAGA,CAAS;AAAA,IAAA;AAGpB,UAAQA,GAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,QACL,WAAAA;AAAA,QACA,QAAQ,eAAeQ,CAAO,OAAOC,CAAK,IAAIH,EAAK,IAAI,IAAI,aAAaA,EAAK,IAAI,IAAI;AAAA,QACrF,WAAW,EAAE,KAAAH,EAAA;AAAA,QACb,QAAQ;AAAA,QACR,MAAMM;AAAA,MAAA;AAAA,IAEV,KAAK;AACH,aAAO;AAAA,QACL,WAAAT;AAAA,QACA,QAAQ,qBAAqBC,CAAQ,2BAA2BS,CAAW;AAAA,QAC3E,WAAW,EAAE,OAAO,CAAC,EAAE,GAAGH,GAAO,CAACD,EAAK,IAAI,IAAI,GAAGH,EAAA,CAAK,EAAA;AAAA,QACvD,QAAQ;AAAA,QACR,MAAM,SAASO,CAAW;AAAA,MAAA;AAAA,IAE9B,KAAK;AACH,aAAO;AAAA,QACL,WAAAV;AAAA,QACA,QAAQ,kBAAkBQ,CAAO,cAAcP,CAAQ,yBAAyBA,CAAQ,IAAIK,EAAK,IAAI,IAAI,8BAA8BG,CAAK;AAAA,QAC5I,WAAW,EAAE,KAAAN,GAAK,QAAQI,EAAA;AAAA,QAC1B,QAAQ;AAAA,QACR,MAAM,SAASN,CAAQ;AAAA,QACvB,OAAO,CAACW,MAAMA,EAAEH,CAAK,KAAK;AAAA,MAAA;AAAA,IAE9B,KAAK;AACH,aAAO;AAAA,QACL,WAAAT;AAAA,QACA,QAAQ,kBAAkBQ,CAAO,aAAaP,CAAQ,IAAIK,EAAK,IAAI,IAAI;AAAA,QACvE,WAAW,EAAE,KAAAH,EAAA;AAAA,QACb,QAAQ;AAAA,QACR,MAAM,SAASF,CAAQ;AAAA,QACvB,OAAO,CAACW,MAAM,OAAOA,EAAE,gBAAmB,CAAC,IAAI;AAAA,MAAA;AAAA,EACjD;AAEJ,QAAMC,IAAQP,EAAK,OAAO,IAAIJ,CAAU;AACxC,MAAIW,GAAO,SAAS;AAClB,UAAM,IAAI,MAAM,GAAGZ,CAAQ,IAAIC,CAAS,8BAA8B;AAExE,QAAMY,IAAYtD,EAAK,MAAM,MAAM,IAAIqD,EAAM,MAAM,GAAG,IAAI,QAAQ,OAC5DE,IACJf,MAAc,YACV;AAAA,IACE,SAASa,EAAM,OACX,CAAC,EAAE,CAACC,CAAS,GAAGV,GAAO,GAAIC,IAAO,EAAE,MAAME,EAAA,IAAU,GAAC,CAAI,IACzD,EAAE,CAACO,CAAS,GAAGV,GAAO,GAAIC,IAAO,EAAE,MAAME,EAAA,IAAU,CAAA,EAAC;AAAA,EAAG,IAE7DP,MAAc,eACZ,EAAE,YAAYa,EAAM,OAAO,CAACT,CAAK,IAAI,GAAA;AAAA;AAAA,IAErC;AAAA,MACE,QAAQS,EAAM,OACV,CAAC,EAAE,CAACC,CAAS,GAAGV,GAAO,MAAMG,EAAA,CAAO,IACpC,EAAE,MAAMA,EAAA;AAAA,IAAM;AAAA;AAE5B,SAAO;AAAA,IACL,WAAAP;AAAA,IACA,QAAQ,kBAAkBQ,CAAO,cAAcP,CAAQ,yBAAyBA,CAAQ,IAAIK,EAAK,IAAI,IAAI,8BAA8BG,CAAK;AAAA,IAC5I,WAAW,EAAE,KAAAN,GAAK,QAAQ,EAAE,CAACD,CAAU,GAAGa,IAAO;AAAA,IACjD,QAAQ;AAAA,IACR,MAAM,SAASd,CAAQ;AAAA;AAAA;AAAA,IAGvB,OAAO,CAACW,MACNA,EAAEH,CAAK,KAAK,SACXT,MAAc,gBACb;AAAA,MACGY,EAAE,MACD,wBACG;AAAA,IAAA,IACH;AAAA,EAAA;AAEZ;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loradb/lora-graphql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "Schema-first GraphQL for LoraDB: one annotated SDL becomes an executable, index-aware GraphQL API that compiles every operation to a single Cypher statement.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"packageManager": "yarn@4.5.1",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
"bench:load": "node bench/load/main.mjs"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
|
-
"@loradb/lora-node": "^0.
|
|
68
|
+
"@loradb/lora-node": "^0.24.1",
|
|
69
69
|
"graphql": "^16.8.0 || ^17.0.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|