@loradb/lora-graphql 0.23.0 → 0.24.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.
Files changed (85) hide show
  1. package/README.md +42 -10
  2. package/dist/analyze/access-lint.d.ts +7 -0
  3. package/dist/analyze/access.d.ts +2 -30
  4. package/dist/analyze/assert-schema.d.ts +19 -0
  5. package/dist/analyze/diff.d.ts +1 -1
  6. package/dist/analyze/plans.d.ts +1 -1
  7. package/dist/analyze/principals.d.ts +25 -0
  8. package/dist/analyze/verdicts.d.ts +45 -0
  9. package/dist/cli.js +2 -2
  10. package/dist/cli.js.map +1 -1
  11. package/dist/compile/cache.d.ts +1 -1
  12. package/dist/compile/read/abstract.d.ts +19 -0
  13. package/dist/compile/read/aggregates.d.ts +34 -0
  14. package/dist/compile/read/by-keys.d.ts +10 -0
  15. package/dist/compile/read/common.d.ts +30 -0
  16. package/dist/compile/read/connection.d.ts +26 -0
  17. package/dist/compile/read/cypher-field.d.ts +21 -0
  18. package/dist/compile/read/match.d.ts +11 -0
  19. package/dist/compile/read/project.d.ts +15 -0
  20. package/dist/compile/read/root.d.ts +5 -0
  21. package/dist/compile/read/search.d.ts +16 -0
  22. package/dist/compile/read/sort.d.ts +42 -0
  23. package/dist/compile/read/types.d.ts +61 -0
  24. package/dist/{diff-B9iDdELF.js → diff-aDivqYqr.js} +2 -2
  25. package/dist/diff-aDivqYqr.js.map +1 -0
  26. package/dist/{driver-C5dSoSD8.js → driver-DSnytFsU.js} +11092 -10674
  27. package/dist/driver-DSnytFsU.js.map +1 -0
  28. package/dist/execute/cypher-mutation.d.ts +1 -1
  29. package/dist/execute/mutate/adjust.d.ts +5 -0
  30. package/dist/execute/mutate/checks.d.ts +21 -0
  31. package/dist/execute/mutate/delete.d.ts +7 -0
  32. package/dist/execute/mutate/env.d.ts +73 -0
  33. package/dist/execute/mutate/errors.d.ts +7 -0
  34. package/dist/execute/mutate/fragments.d.ts +37 -0
  35. package/dist/execute/mutate/plan.d.ts +95 -0
  36. package/dist/execute/mutate/runner.d.ts +129 -0
  37. package/dist/execute/mutate/unique-together.d.ts +18 -0
  38. package/dist/execute/mutate.d.ts +2 -79
  39. package/dist/index.d.ts +10 -6
  40. package/dist/index.js +2 -2
  41. package/dist/lora-graphql.d.ts +12 -326
  42. package/dist/model/build/authorization.d.ts +31 -0
  43. package/dist/model/build/cypher-field.d.ts +11 -0
  44. package/dist/model/build/directive-args.d.ts +16 -0
  45. package/dist/model/build/limits.d.ts +18 -0
  46. package/dist/model/build/relationship-field.d.ts +11 -0
  47. package/dist/model/build/rule-checks.d.ts +23 -0
  48. package/dist/model/build/rule-references.d.ts +28 -0
  49. package/dist/model/build/scalar-field.d.ts +7 -0
  50. package/dist/model/build/search.d.ts +4 -0
  51. package/dist/model/build/shapes.d.ts +18 -0
  52. package/dist/model/build/unique-together.d.ts +5 -0
  53. package/dist/model/build.d.ts +3 -20
  54. package/dist/model/directives.d.ts +1 -1
  55. package/dist/model/types.d.ts +12 -2
  56. package/dist/options.d.ts +255 -0
  57. package/dist/results.d.ts +70 -0
  58. package/dist/runtime/custom-requires.d.ts +7 -0
  59. package/dist/runtime/database-error.d.ts +10 -0
  60. package/dist/runtime/documents.d.ts +35 -0
  61. package/dist/runtime/limits.d.ts +37 -0
  62. package/dist/runtime/request.d.ts +33 -0
  63. package/dist/runtime/timing.d.ts +13 -0
  64. package/dist/schema/aggregates.d.ts +17 -0
  65. package/dist/schema/base.d.ts +17 -0
  66. package/dist/schema/build.d.ts +3 -42
  67. package/dist/schema/filters.d.ts +10 -0
  68. package/dist/schema/hooks.d.ts +42 -0
  69. package/dist/schema/objects.d.ts +36 -0
  70. package/dist/schema/query.d.ts +8 -0
  71. package/dist/schema/resolvers.d.ts +13 -0
  72. package/dist/schema/sorts.d.ts +5 -0
  73. package/dist/schema/subscriptions.d.ts +7 -0
  74. package/dist/subscriptions/change-hub.d.ts +50 -0
  75. package/dist/subscriptions/compiles.d.ts +23 -0
  76. package/dist/subscriptions/events.d.ts +9 -0
  77. package/dist/subscriptions/filter-cost.d.ts +15 -0
  78. package/dist/subscriptions/subscriptions.d.ts +73 -0
  79. package/dist/testing.d.ts +4 -1
  80. package/dist/testing.js +34 -29
  81. package/dist/testing.js.map +1 -1
  82. package/package.json +2 -2
  83. package/dist/compile/read.d.ts +0 -102
  84. package/dist/diff-B9iDdELF.js.map +0 -1
  85. package/dist/driver-C5dSoSD8.js.map +0 -1
package/README.md CHANGED
@@ -746,18 +746,24 @@ type Claims @jwt {
746
746
  roles: [String!] @jwtClaim(path: "app_metadata.roles")
747
747
  }
748
748
 
749
+ extend schema @authorizationDefaults(requireAuthentication: false)
750
+
749
751
  type Post
750
752
  @node
751
753
  @mutation
752
754
  @authentication(operations: [CREATE, UPDATE, DELETE])
753
755
  @authorization(
754
756
  filter: [
757
+ { where: { node: { published: { eq: true } } } }
758
+ { where: { node: { author: { key: { eq: "$jwt.sub" } } } } }
755
759
  {
756
- where: { node: { published: { eq: true } } }
757
- requireAuthentication: false
760
+ where: {
761
+ AND: [
762
+ { jwt: { sub: { exists: true } } }
763
+ { node: { tenant: { eq: "$context.tenant" } } }
764
+ ]
765
+ }
758
766
  }
759
- { where: { node: { author: { key: { eq: "$jwt.sub" } } } } }
760
- { where: { node: { tenant: { eq: "$context.tenant" } } } }
761
767
  { where: { jwt: { roles: { includes: "admin" } } } }
762
768
  ]
763
769
  validate: [
@@ -791,19 +797,33 @@ type Post
791
797
  }
792
798
  ```
793
799
 
800
+ - A rule takes `operations`, `when` (validate rules) and `where`; any
801
+ other field is a model error, so a misspelt `operations` cannot fall
802
+ back to the default unnoticed.
794
803
  - A rule is `{ node, jwt, AND, OR, NOT }`. `node` is a filter over the type
795
804
  (relationship properties included, through `<field>Connection: { some:
796
805
  { node, edge } }`), where `"$jwt.path"` strings become the caller's claims and
797
806
  `"$context.path"` strings values from the GraphQL context. `jwt` tests
798
807
  claims (`eq`, `in`, `includes`, `contains`, `startsWith`, `endsWith`,
799
808
  `lt`, `lte`, `gt`, `gte`, `exists`).
809
+ - **Anonymous callers.** Without a token every rule denies, unless the
810
+ schema says `@authorizationDefaults(requireAuthentication: false)`. Then
811
+ a rule with a branch that reads no claims decides that branch for
812
+ anonymous callers too (the published posts above), while a rule that
813
+ needs claims still asks for a token: `UNAUTHENTICATED`, not `FORBIDDEN`.
814
+ It is one setting for the schema; a rule has no such field. To keep a
815
+ claim-free branch for signed-in callers, test a claim beside it
816
+ (`{ jwt: { sub: { exists: true } } }`, as the tenant rule above does),
817
+ or put `@authentication` on the type.
800
818
  - **The caller's own node.** Mark the claim that identifies the caller
801
819
  with `@viewer(type: "Person", field: "subject")` (the field is `@key` or
802
820
  `@unique`), so the key can stay a slug while the subject is an identity
803
821
  provider's opaque id. Rules then say `{ node: { isViewer: true } }` on
804
822
  the viewer type, or `{ node: { author: { isViewer: true } } }` through a
805
823
  relationship; this expands to `{ subject: { eq: "$jwt.sub" } }` at
806
- startup and compiles to exactly the same statement. `viewer: { verified:
824
+ startup and compiles to exactly the same statement. With `@viewer`
825
+ declared, writing that expansion out by hand is a model error: the
826
+ mapping lives in one place. `viewer: { verified:
807
827
  { eq: true } }` tests the caller's own node: one seek by the claim. Both
808
828
  are unknown without the claim, so `NOT { isViewer: true }` never grants a
809
829
  signed-out caller. `isViewer` takes `true` only; use `NOT` for the
@@ -1019,6 +1039,8 @@ deleted: Org.docs has onDelete: RESTRICT`. Replacing a single
1019
1039
  covers an operation, it replaces the default for that operation, never
1020
1040
  merges with it. `check()` fails on a `@mutation` type whose
1021
1041
  writes nothing guards, unless it says `@authorization(public: [...])`.
1042
+ `requireAuthentication` (see "Anonymous callers" above) is the third
1043
+ setting here.
1022
1044
 
1023
1045
  Relationship and `@cypher` fields take field-level `@authorization`
1024
1046
  with READ validate rules: a row failing the rule reads the field as
@@ -1105,7 +1127,10 @@ type Trip @node @mutation {
1105
1127
  - The rules hold whichever side the write comes from: a connect through
1106
1128
  `Person.trips` (the same relationship type, the other direction)
1107
1129
  answers to `Trip.members`' rules, with source and target as declared on
1108
- `Trip.members`. If both fields carry rules, both apply.
1130
+ `Trip.members`. Declare each operation's rules on one of the two
1131
+ fields: the same operation ruled on both is a model error (combine
1132
+ with `AND` where both must pass); different operations may sit on
1133
+ different sides.
1109
1134
  - A relationship failing `READ_EDGE` reads its properties as `FORBIDDEN`.
1110
1135
  Unless the claims alone settle the rule, nothing may filter, sort or
1111
1136
  aggregate by that relationship's properties.
@@ -1417,7 +1442,11 @@ relationship property (READ, CREATE, UPDATE) and root `@cypher` field,
1417
1442
  each operation as each kind of caller (anonymous, authenticated, and each role
1418
1443
  the rules test, such as `roles:admin`), with the verdict (`allowed`,
1419
1444
  `filtered`, `validated`, `masked`, `denied`, `unauthenticated`) and the
1420
- rules that decide it. A `@key(scope: VIEWER)` type's CREATE reads
1445
+ rules that decide it. When rules name the caller's node (`isViewer`),
1446
+ `viewer` is the caller on nodes that are theirs: every rule part that
1447
+ only names them passes, so it shows the most a related caller gets,
1448
+ where `authenticated` shows what any signed-in caller gets. A
1449
+ `@key(scope: VIEWER)` type's CREATE reads
1421
1450
  `unauthenticated` for anonymous callers and `validated` by `key scope`
1422
1451
  for the rest.
1423
1452
 
@@ -1441,10 +1470,13 @@ order is stable, so a snapshot in CI turns access changes into diffs.
1441
1470
  `check` lints authorization too:
1442
1471
 
1443
1472
  - a filter rule every signed-in caller passes;
1444
- - a rule whose default `requireAuthentication` refuses anonymous callers
1445
- a branch that needs no claims;
1473
+ - a rule with a branch that needs no claims, in a schema that leaves
1474
+ `@authorizationDefaults(requireAuthentication:)` unset: anonymous
1475
+ callers are refused that branch by default, so say which was meant;
1446
1476
  - field rules the schema's bypass skips, on a type that says neither
1447
- `@authorization(bypass: false)` nor `bypass: true`;
1477
+ `@authorization(bypass: false)` nor `bypass: true`, and one line naming
1478
+ every type with rules the bypass reaches that way (plus relationship
1479
+ property types, whose rules cannot opt out);
1448
1480
  - an UPDATE rule testing a single relationship the update input can
1449
1481
  re-point (`connect`, `disconnect`, `create`): the rule sees the node
1450
1482
  before (and, validated `AFTER`, after) the write, so a caller passing it
@@ -0,0 +1,7 @@
1
+ import { GraphQLSchema } from 'graphql';
2
+ import { GraphModel, ModelWarning } from '../model/types.js';
3
+ /**
4
+ * Authorization lints for `check`: valid rules that are probably not what
5
+ * was meant.
6
+ */
7
+ export declare function accessLints(model: GraphModel, schema: GraphQLSchema): ModelWarning[];
@@ -1,29 +1,6 @@
1
1
  import { FragmentDefinitionNode, GraphQLSchema, OperationDefinitionNode } from 'graphql';
2
- import { AuthOperation, GraphModel, ModelWarning } from '../model/types.js';
3
- export type AccessVerdict =
4
- /** Every row, for this caller. */
5
- "allowed"
6
- /** Only rows a filter rule admits; others are invisible. */
7
- | "filtered"
8
- /** Checked per row; a failing row is FORBIDDEN. */
9
- | "validated"
10
- /** Rows failing a mask read a substitute value. */
11
- | "masked"
12
- /** Never, for this caller. */
13
- | "denied"
14
- /** Needs a token this caller does not have. */
15
- | "unauthenticated";
16
- export interface AccessEntry {
17
- type: string;
18
- /** The field, for field-level and relationship rules. */
19
- field?: string;
20
- /** `EXECUTE`: running a Mutation @cypher field. */
21
- operation: AuthOperation | "EXECUTE";
22
- principal: string;
23
- verdict: AccessVerdict;
24
- /** What decides it: `filter[0]`, `validate[1]`, `@authentication`, `bypass`, … */
25
- by: string[];
26
- }
2
+ import { AuthOperation, GraphModel } from '../model/types.js';
3
+ import { AccessEntry, AccessVerdict } from './verdicts.js';
27
4
  /** One root field of an operation, as each kind of caller. */
28
5
  export interface RootFieldAccess {
29
6
  /** The root field. */
@@ -68,8 +45,3 @@ export interface OperationAccess {
68
45
  export declare function operationAccess(model: GraphModel, schema: GraphQLSchema, operation: OperationDefinitionNode, fragments: ReadonlyMap<string, FragmentDefinitionNode>): OperationAccess;
69
46
  /** The access matrix of `model`, in a stable order. */
70
47
  export declare function accessMatrix(model: GraphModel, schema: GraphQLSchema): AccessEntry[];
71
- /**
72
- * Authorization lints for `check`: valid rules that are probably not what
73
- * was meant.
74
- */
75
- export declare function accessLints(model: GraphModel, schema: GraphQLSchema): ModelWarning[];
@@ -0,0 +1,19 @@
1
+ import { LoraDriver } from '../driver.js';
2
+ import { AssertSchemaOptions } from '../options.js';
3
+ import { CheckReport, SchemaAssertion } from '../results.js';
4
+ import { SchemaRequirement } from './indexes.js';
5
+ export interface SchemaEnv {
6
+ driver: LoraDriver;
7
+ /** Timeout for every statement, in milliseconds. */
8
+ timeoutMs: number;
9
+ /** Constraints and indexes the API needs. */
10
+ requirements(): SchemaRequirement[];
11
+ }
12
+ /**
13
+ * Verify the database has every constraint and index the API needs;
14
+ * with `create`, add what is missing. Idempotent. Runs one DDL
15
+ * statement at a time: LoraDB rejects schema commands in transactions.
16
+ */
17
+ export declare function assertSchema(env: SchemaEnv, options: AssertSchemaOptions): Promise<SchemaAssertion>;
18
+ /** Indexes in the database that no requirement of the API matches. */
19
+ export declare function unusedIndexes(env: SchemaEnv): Promise<CheckReport["unused"]>;
@@ -1,5 +1,5 @@
1
1
  import { DocumentNode } from 'graphql';
2
- import { ModelOptions } from '../model/build.js';
2
+ import { ModelOptions } from '../model/build/limits.js';
3
3
  import { SchemaRequirement } from './indexes.js';
4
4
  export interface ApiChange {
5
5
  /** graphql-js change type, e.g. `FIELD_REMOVED`. */
@@ -1,5 +1,5 @@
1
1
  import { LoraDriver, QueryPlan, Statement } from '../driver.js';
2
- import { CompiledRead } from '../compile/read.js';
2
+ import { CompiledRead } from '../compile/read/types.js';
3
3
  export interface PlanFinding {
4
4
  rule: "full-scan" | "mutating-read" | "result-columns" | "scan-expand" | "row-budget";
5
5
  message: string;
@@ -0,0 +1,25 @@
1
+ import { Field, GraphModel, NodeType, RelationshipPropertiesType } from '../model/types.js';
2
+ export interface Principal {
3
+ name: string;
4
+ jwt: Record<string, unknown> | undefined;
5
+ /** The model this principal's verdicts are read off, when not the schema's. */
6
+ model?: GraphModel;
7
+ }
8
+ /** `node` (or its field, or a property type's field) as `p` sees the model. */
9
+ export declare const nodeAs: (p: Principal, node: NodeType) => NodeType;
10
+ export declare const fieldAs: <F extends Field>(p: Principal, node: NodeType, f: F) => F;
11
+ export declare const propertyAs: <F extends {
12
+ name: string;
13
+ }>(p: Principal, props: RelationshipPropertiesType, f: F) => F;
14
+ /**
15
+ * Anonymous, authenticated (a token with no roles), `viewer` when rules
16
+ * name the caller's node, and one principal per claim value the rules
17
+ * test with `includes`, `eq` or `in` (`roles:admin`).
18
+ */
19
+ export declare function principalsOf(model: GraphModel): Principal[];
20
+ /**
21
+ * A token carrying `claims` where the `@jwt` type says they live
22
+ * (`@jwtClaim(path: "app_metadata.roles")`).
23
+ */
24
+ export declare function token(model: GraphModel, claims: Record<string, unknown>): Record<string, unknown>;
25
+ export declare const isRecord: (x: unknown) => x is Record<string, unknown>;
@@ -0,0 +1,45 @@
1
+ import { fieldReadGuard, propertyAccess, relationshipRules } from '../compile/auth.js';
2
+ import { CompileContext } from '../compile/context.js';
3
+ import { AuthOperation, CypherField, Field, NodeType, RelationshipOperation } from '../model/types.js';
4
+ export type AccessVerdict =
5
+ /** Every row, for this caller. */
6
+ "allowed"
7
+ /** Only rows a filter rule admits; others are invisible. */
8
+ | "filtered"
9
+ /** Checked per row; a failing row is FORBIDDEN. */
10
+ | "validated"
11
+ /** Rows failing a mask read a substitute value. */
12
+ | "masked"
13
+ /** Never, for this caller. */
14
+ | "denied"
15
+ /** Needs a token this caller does not have. */
16
+ | "unauthenticated";
17
+ export interface AccessEntry {
18
+ type: string;
19
+ /** The field, for field-level and relationship rules. */
20
+ field?: string;
21
+ /** `EXECUTE`: running a Mutation @cypher field. */
22
+ operation: AuthOperation | "EXECUTE";
23
+ principal: string;
24
+ verdict: AccessVerdict;
25
+ /** What decides it: `filter[0]`, `validate[1]`, `@authentication`, `bypass`, … */
26
+ by: string[];
27
+ }
28
+ export declare function rootFieldVerdict(ctx: CompileContext, field: CypherField): Pick<AccessEntry, "verdict" | "by">;
29
+ export declare const PROPERTY_OPS: readonly ["READ", "CREATE", "UPDATE"];
30
+ /** `field` has rules (or `@authentication`) for `op`. */
31
+ export declare function fieldWriteGuarded(field: {
32
+ authentication?: ReadonlySet<AuthOperation> | undefined;
33
+ authorization?: NodeType["authorization"];
34
+ }, op: AuthOperation): boolean;
35
+ export declare function fieldWriteVerdict(ctx: CompileContext, node: NodeType, field: Field & {
36
+ kind: "scalar";
37
+ }, op: "CREATE" | "UPDATE"): Pick<AccessEntry, "verdict" | "by">;
38
+ export declare function propertyVerdict(ctx: CompileContext, field: Parameters<typeof propertyAccess>[1], op: AuthOperation): Pick<AccessEntry, "verdict" | "by">;
39
+ export declare function typeVerdict(ctx: CompileContext, node: NodeType, op: AuthOperation): Pick<AccessEntry, "verdict" | "by">;
40
+ export declare function fieldVerdict(ctx: CompileContext, node: NodeType, field: Parameters<typeof fieldReadGuard>[2] & {
41
+ name: string;
42
+ kind: string;
43
+ authorization?: NodeType["authorization"];
44
+ }): Pick<AccessEntry, "verdict" | "by">;
45
+ export declare function relationshipVerdict(ctx: CompileContext, node: NodeType, field: Parameters<typeof relationshipRules>[1], op: RelationshipOperation): Pick<AccessEntry, "verdict" | "by">;
package/dist/cli.js CHANGED
@@ -3,8 +3,8 @@ import { execFileSync as re } from "node:child_process";
3
3
  import { stat as M, readFile as ce, readdir as ue, mkdir as le, writeFile as _ } from "node:fs/promises";
4
4
  import { dirname as fe, basename as de, join as O, relative as pe } from "node:path";
5
5
  import { parse as j, Kind as p, visit as Y, print as Z, typeFromAST as me, isNonNullType as G, isListType as he, getNamedType as ge, isEnumType as ve, isInputObjectType as we } from "graphql";
6
- import { d as Ee } from "./diff-B9iDdELF.js";
7
- import { q as K, e as be, M as Te, a as R, l as X, g as x, u as ee, c as Ne, i as ye, r as Se, w as Ie } from "./driver-C5dSoSD8.js";
6
+ import { d as Ee } from "./diff-aDivqYqr.js";
7
+ import { q as K, e as be, M as Te, a as R, l as X, g as x, u as ee, c as Ne, i as ye, r as Se, w as Ie } from "./driver-DSnytFsU.js";
8
8
  const $e = /* @__PURE__ */ new Set(["Query", "Mutation", "Subscription"]), W = {
9
9
  coalesce: "@coalesce is not supported: it hides predicates from indexes; store a value instead",
10
10
  subscriptionsAuthorization: "@subscriptionsAuthorization is not supported: SUBSCRIBE rules go in @authorization",