@loradb/lora-graphql 0.25.0 → 0.25.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 CHANGED
@@ -156,26 +156,26 @@ Model:
156
156
 
157
157
  API:
158
158
 
159
- | Directive | On | Meaning |
160
- | ----------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
161
- | `@query(read:, aggregate:)` | type, interface, union | Generated reads |
162
- | `@mutation(operations: [CREATE, UPDATE, DELETE])` | type | Generated mutations; none without it |
163
- | `@subscription(operations: [CREATE, UPDATE, DELETE])` | type | Generated subscriptions; none without it |
164
- | `@filterable(byValue: [...])` | field | Filter operators. Bare: `EQ` and `IN` (lists: `INCLUDES`). On a relationship: enables relationship filters |
165
- | `@sortable` | field | Sort and paginate by this field (on a relationship property: `sort: [{ edge: { ... } }]`) |
166
- | `@groupBy` | field | A grouping key of `<plural>Grouped(by:)` (needs `@query(aggregate: true)`) |
167
- | `@limit(default:, max:)` | type, interface, union, list relationship | Page size bounds |
168
- | `@length(min:, max:)` | String / ID field | Its length in code points (each item of a list) |
169
- | `@pattern(regex:)` | String / ID field | An expression the whole value must match; needs `@length(max:)` |
170
- | `@format(as:)` | String / ID field | `email`, `email-strict`, `url`, `uuid`, `hostname`, `ipv4`, `ipv6`, `slug`, `hex-color`, `ulid`, `e164` |
171
- | `@range(min:, max:, exclusiveMin:, exclusiveMax:)` | number field; argument of a `@cypher` field | Bounds of its value (each item of a list); on an argument, `min` and `max` |
172
- | `@multipleOf(value:)` | Int / Float field | A whole multiple of `value` |
173
- | `@size(min:, max:, unique:)` | list field; list argument of `@cypher` | How many items it holds; on an argument, `max` (default `maxListArgument`) |
174
- | `@relayId` | `@key` field | Adds a global `id` and the `Node` interface |
175
- | `@authentication(operations:, jwt:)` | type, field | Needs an authenticated request, whose claims satisfy `jwt` |
176
- | `@authorization(filter:, validate:)` | type (filter and validate), field (validate) | Row-level rules, compiled into statements |
177
- | `@jwt`, `@jwtClaim(path:)` | type, field | The claims shape; rules may only use declared claims |
178
- | `@viewer(type:, field:)` | `@jwt` claim | The claim naming the caller's node (by a `@key` or `@unique` field): enables `isViewer` and `viewer` in rules |
159
+ | Directive | On | Meaning |
160
+ | ----------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `@query(read:, aggregate:)` | type, interface, union | Generated reads |
162
+ | `@mutation(operations: [CREATE, UPDATE, DELETE])` | type | Generated mutations; none without it |
163
+ | `@subscription(operations: [CREATE, UPDATE, DELETE])` | type | Generated subscriptions; none without it |
164
+ | `@filterable(byValue: [...])` | field | Filter operators. Bare: `EQ` and `IN` (lists: `INCLUDES`). On a relationship: enables relationship filters |
165
+ | `@sortable` | field | Sort and paginate by this field (on a relationship property: `sort: [{ edge: { ... } }]`) |
166
+ | `@groupBy` | field | A grouping key of `<plural>Grouped(by:)` (needs `@query(aggregate: true)`) |
167
+ | `@limit(default:, max:)` | type, interface, union, list relationship | Page size bounds |
168
+ | `@length(min:, max:)` | String / ID field | Its length in code points (each item of a list) |
169
+ | `@pattern(regex:)` | String / ID field | An expression the whole value must match; needs `@length(max:)` |
170
+ | `@format(as:)` | String / ID field | `email`, `email-strict`, `url`, `uuid`, `hostname`, `ipv4`, `ipv6`, `slug`, `hex-color`, `ulid`, `e164` |
171
+ | `@range(min:, max:, exclusiveMin:, exclusiveMax:)` | number field; argument of a `@cypher` field | Bounds of its value (each item of a list); on an argument, `min` and `max` |
172
+ | `@multipleOf(value:)` | Int / Float field | A whole multiple of `value` |
173
+ | `@size(min:, max:, unique:)` | list field; list argument of `@cypher`; list relationship | How many items it holds; on an argument, `max` (default `maxListArgument`); on a relationship, `max` items per nested `connect`, `create`, `disconnect` and `update` list |
174
+ | `@relayId` | `@key` field | Adds a global `id` and the `Node` interface |
175
+ | `@authentication(operations:, jwt:)` | type, field | Needs an authenticated request, whose claims satisfy `jwt` |
176
+ | `@authorization(filter:, validate:)` | type (filter and validate), field (validate) | Row-level rules, compiled into statements |
177
+ | `@jwt`, `@jwtClaim(path:)` | type, field | The claims shape; rules may only use declared claims |
178
+ | `@viewer(type:, field:)` | `@jwt` claim | The claim naming the caller's node (by a `@key` or `@unique` field): enables `isViewer` and `viewer` in rules |
179
179
 
180
180
  `directiveTypeDefs` (or `lora-graphql directives`) prints these as SDL for
181
181
  editors and codegen.
@@ -207,7 +207,8 @@ Email @storedAs(type: STRING) @format(as: "email")`), for every field of
207
207
  that type. For a rule across fields, the `validators` option takes a
208
208
  Standard Schema validator per node type (zod, valibot, arktype, yup),
209
209
  run on each input after the field rules. The same directives go on
210
- `@cypher` arguments. Each input field says its rules in its description,
210
+ `@cypher` arguments, `@normalize` included: it changes the argument
211
+ before its rules are checked and before the statement sees it. Each input field says its rules in its description,
211
212
  `lora.inputSchemas()` (CLI: `json-schema`) exports them as JSON Schema
212
213
  for clients, `diff` reports a tightened rule as breaking, and `check`
213
214
  lists stored values that no longer pass. See the
@@ -549,7 +550,11 @@ entry), and a delete reaches at most `maxBatch` nodes through
549
550
  `onDelete: CASCADE`. Relationships written (connects, each key of a
550
551
  `disconnect` list, nested `update` entries) count up to ten times
551
552
  `maxBatch`. Going over is `LIMIT_EXCEEDED`, before the writes that
552
- would exceed it. `@key` is not updatable.
553
+ would exceed it. For a bound per field, `@size(max:)` on a list
554
+ relationship (`following: [Person!]! @relationship(...) @size(max: 50)`)
555
+ caps each nested `connect`, `create`, `disconnect` and `update` list of
556
+ one input; a longer list is `BAD_USER_INPUT` before the transaction
557
+ opens. `@key` is not updatable.
553
558
  `info` reports `nodesCreated`, `nodesUpdated`, `nodesDeleted`,
554
559
  `relationshipsCreated` and `relationshipsDeleted`.
555
560
 
@@ -671,6 +676,22 @@ inferred when the statement's last top-level `RETURN` has one item,
671
676
  Fields returning `@node` types are projected with the selection like any
672
677
  other node, and read filters apply to them.
673
678
 
679
+ A `Mutation` statement says what it wrote in two more columns, read as a
680
+ trigger statement's are: the nodes it returns as `created` and `updated`
681
+ are its write-set, so its change is not `broad` and subscriptions on
682
+ their types fire. Without either column the change is `broad`, and
683
+ `check()` lints the mutation.
684
+
685
+ ```graphql
686
+ type Mutation {
687
+ addGenre(key: String!, name: String!): Genre
688
+ @cypher(
689
+ statement: "CREATE (g:Genre {key: $key, name: $name}) RETURN g, g AS created"
690
+ columnName: "g"
691
+ )
692
+ }
693
+ ```
694
+
674
695
  Statements are checked, not trusted:
675
696
 
676
697
  - at startup: every `$parameter` must be an argument, `$jwt` or
@@ -752,7 +773,10 @@ createPost(key: String!, hashtags: [String!] = [] @size(max: 30)): Post
752
773
  Int and Float arguments take bounds the same way: `@range(min:, max:)`
753
774
  (either or both, inclusive) checks the value, or each item of a list,
754
775
  before the statement runs, and a default outside the bounds is a model
755
- error. Null is not checked; that is the type's business.
776
+ error. Null is not checked; that is the type's business. A String or ID
777
+ argument takes `@normalize` (`text: String! @normalize(trim: true)
778
+ @length(min: 1, max: 280)`): the trimmed value is what `@length` counts
779
+ and what the statement gets.
756
780
 
757
781
  ```graphql
758
782
  nearby(lat: Float! @range(min: -90, max: 90), km: Int = 10 @range(min: 1, max: 500)): [Festival!]!
@@ -1280,8 +1304,8 @@ reported as an API break with no data migration.
1280
1304
  ```ts
1281
1305
  lora.onWrite((change) => {
1282
1306
  // Per type: lists, counts and connections of these types may have
1283
- // changed, not only the entities. A broad change (a @cypher mutation)
1284
- // names nothing, so it invalidates every type.
1307
+ // changed, not only the entities. A broad change (a @cypher mutation
1308
+ // that does not say what it wrote) names nothing: every type goes.
1285
1309
  const types = change.broad ? allNodeTypes : change.types;
1286
1310
  cache.invalidate(types.map((typename) => ({ typename })));
1287
1311
  });
@@ -1295,7 +1319,8 @@ relationships `connected` and `disconnected` (by field and both keys),
1295
1319
  included), and the touched `types` and `relationshipTypes`. Every compiled
1296
1320
  read carries a read-set (labels and relationship types);
1297
1321
  `lora.affects(reads, change)` tells whether a cached read may be stale.
1298
- `@cypher` mutations have no known write-set and are reported with
1322
+ A `@cypher` mutation's write-set is the nodes its statement returns as
1323
+ `created` and `updated`; one that returns neither is reported with
1299
1324
  `broad: true`. Only writes made through the library are seen. A consumer
1300
1325
  that falls `maxQueuedChanges` (default 1000) behind is ended with an error
1301
1326
  rather than buffering without bound.
@@ -1467,6 +1492,12 @@ lora.worker("Notification", {
1467
1492
  every node of the type the mutation wrote; `$jwt`, `$viewer` and
1468
1493
  `$operation` are bound. `fields:` narrows an `UPDATE` trigger to
1469
1494
  updates that named one of them.
1495
+ - `@trigger(on: [CONNECT], field: "following", statement: ...)` runs
1496
+ once per relationship the mutation created through that field, from
1497
+ either side, with `this` and `target` bound: "tell a person who
1498
+ followed them" without reading any other relationship. `DISCONNECT`
1499
+ does the same for a relationship removed. A trigger function reads
1500
+ them in `change.connected` and `change.disconnected`.
1470
1501
  - Nodes a statement returns as `created` or `updated` join the
1471
1502
  mutation's write-set, so `onWrite` and subscriptions see them without
1472
1503
  `changeFeed`.
@@ -1722,38 +1753,38 @@ transaction. Mutations need interactive transactions, which only the Node
1722
1753
  binding has, so the WASM binding serves reads.
1723
1754
  `explain()`, and so plan checks, need the Node binding too.
1724
1755
 
1725
- | Option | Default | Meaning |
1726
- | ---------------------------- | ------------- | -------------------------------------------------------- |
1727
- | `timeoutMs` | 10 000 | Per statement and lock wait; a `signal` cancels |
1728
- | `operationTimeoutMs` | 2 × timeout | All root fields of one query; then `TIMEOUT` |
1729
- | `maxConcurrentStatements` | 2 | Statements one operation runs at once |
1730
- | `maxCost` | 50 000 | Estimated rows per operation |
1731
- | `maxBatch` | 1000 | Nodes written per mutation; bulk `limit` by default |
1732
- | `compileCacheBytes` | 64 MiB | Compile cache and document cache, each |
1733
- | `maxQueuedChanges` | 1000 | How far a change consumer may fall behind |
1734
- | `maxSubscriptions` | 100 | Live subscriptions per `subscriptionScope` |
1735
- | `maxQueuedWrites` | 512 | Writes waiting for the writer lock; more is `OVERLOADED` |
1736
- | `maxTotalCount` | none | Where a root connection's `totalCount` stops counting |
1737
- | `triggerTimeoutMs` | 1000 | Per trigger function; past it the mutation fails |
1738
- | `singleCallMutations` | true | A create is one call that begins, runs, commits |
1739
- | `subscriptionScope` | the context | What `maxSubscriptions` counts per |
1740
- | `maxSubscriptionFilterDepth` | 1 | Relationship filter nesting in a subscription where |
1741
- | `subscriptionTimeoutMs` | 2000 | Per statement checking a change for a subscriber |
1742
- | `defaultLimit` / `maxLimit` | 25 / 100 | Global page sizes; `@limit` may only lower `max` |
1743
- | `callbacks` | | Named callbacks for `@populatedBy` |
1744
- | `jwt` | `context.jwt` | Where the claims are |
1745
- | `onStatement` | | Observe every statement |
1746
- | `cursorSecret` | | Sign cursors; reject unsigned or forged ones |
1747
- | `maskErrors` | production | Clients get `DATABASE_ERROR` and an `id` only |
1748
- | `onError` | | Receives each database error's detail and `id` |
1749
- | `guards` | see below | Document limits; `false` turns them off |
1750
- | `persistedOnly` | false | `execute()`, `subscribe()` run persisted ops only |
1751
- | `budget` | | Cost limit per request, from the context |
1752
- | `onCost` | | Each root field's estimate, total and limit |
1753
- | `onStatementEnd` | | Duration, rows and error of every statement call |
1754
- | `timing` | false | `extensions.timing` in `execute()` results |
1755
- | `tracer` / `traceStatements` | | OpenTelemetry-style spans; Cypher text on request |
1756
- | `metrics` | | Counters and histograms (see below) |
1756
+ | Option | Default | Meaning |
1757
+ | ---------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1758
+ | `timeoutMs` | 10 000 | Per statement and lock wait; a `signal` cancels |
1759
+ | `operationTimeoutMs` | 2 × timeout | All root fields of one query; then `TIMEOUT` |
1760
+ | `maxConcurrentStatements` | 2 | Statements one operation runs at once |
1761
+ | `maxCost` | 50 000 | Estimated rows per operation |
1762
+ | `maxBatch` | 1000 | Nodes written per mutation, and ten times as many relationships; bulk `limit` by default. A nested `connect` list is bounded per field by `@size(max:)` on the relationship |
1763
+ | `compileCacheBytes` | 64 MiB | Compile cache and document cache, each |
1764
+ | `maxQueuedChanges` | 1000 | How far a change consumer may fall behind |
1765
+ | `maxSubscriptions` | 100 | Live subscriptions per `subscriptionScope` |
1766
+ | `maxQueuedWrites` | 512 | Writes waiting for the writer lock; more is `OVERLOADED` |
1767
+ | `maxTotalCount` | none | Where a root connection's `totalCount` stops counting |
1768
+ | `triggerTimeoutMs` | 1000 | Per trigger function; past it the mutation fails |
1769
+ | `singleCallMutations` | true | A create is one call that begins, runs, commits |
1770
+ | `subscriptionScope` | the context | What `maxSubscriptions` counts per |
1771
+ | `maxSubscriptionFilterDepth` | 1 | Relationship filter nesting in a subscription where |
1772
+ | `subscriptionTimeoutMs` | 2000 | Per statement checking a change for a subscriber |
1773
+ | `defaultLimit` / `maxLimit` | 25 / 100 | Global page sizes; `@limit` may only lower `max` |
1774
+ | `callbacks` | | Named callbacks for `@populatedBy` |
1775
+ | `jwt` | `context.jwt` | Where the claims are |
1776
+ | `onStatement` | | Observe every statement |
1777
+ | `cursorSecret` | | Sign cursors; reject unsigned or forged ones |
1778
+ | `maskErrors` | production | Clients get `DATABASE_ERROR` and an `id` only |
1779
+ | `onError` | | Receives each database error's detail and `id` |
1780
+ | `guards` | see below | Document limits; `false` turns them off |
1781
+ | `persistedOnly` | false | `execute()`, `subscribe()` run persisted ops only |
1782
+ | `budget` | | Cost limit per request, from the context |
1783
+ | `onCost` | | Each root field's estimate, total and limit |
1784
+ | `onStatementEnd` | | Duration, rows and error of every statement call |
1785
+ | `timing` | false | `extensions.timing` in `execute()` results |
1786
+ | `tracer` / `traceStatements` | | OpenTelemetry-style spans; Cypher text on request |
1787
+ | `metrics` | | Counters and histograms (see below) |
1757
1788
 
1758
1789
  Errors carry `extensions.code`: `BAD_USER_INPUT`, `INVALID_CURSOR`,
1759
1790
  `LIMIT_EXCEEDED`, `COST_EXCEEDED`, `OVERLOADED` (more than
package/dist/cli.js CHANGED
@@ -3,8 +3,8 @@ import { execFileSync as ue } from "node:child_process";
3
3
  import { stat as F, readFile as le, readdir as fe, mkdir as de, writeFile as M } from "node:fs/promises";
4
4
  import { dirname as pe, basename as me, join as k, relative as he } from "node:path";
5
5
  import { parse as x, Kind as p, visit as Z, print as K, typeFromAST as ge, isNonNullType as W, isListType as ve, getNamedType as we, isEnumType as be, isInputObjectType as Ee } from "graphql";
6
- import { d as Te } from "./diff-CdZZG-N1.js";
7
- import { w as X, j as ye, c as ee, e as Ne, M as Se, a as P, l as te, g as C, x as ne, i as $e, r as Ie, y as ke } from "./lora-graphql-D11QFUkf.js";
6
+ import { d as Te } from "./diff-BZZNZs76.js";
7
+ import { w as X, j as ye, c as ee, e as Ne, M as Se, a as P, l as te, g as C, x as ne, i as $e, r as Ie, y as ke } from "./lora-graphql-RvZQdHNA.js";
8
8
  const Oe = /* @__PURE__ */ new Set(["Query", "Mutation", "Subscription"]), z = {
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",
@@ -1,5 +1,5 @@
1
1
  import * as b from "graphql";
2
- import { c as $, i as w, r as E, n as C, o as h, q as N, u as k } from "./lora-graphql-D11QFUkf.js";
2
+ import { c as $, i as w, r as E, n as C, o as h, q as N, u as k } from "./lora-graphql-RvZQdHNA.js";
3
3
  function q(i, n, o = {}) {
4
4
  const c = $(i, o), t = $(n, o), f = C, d = new Map(w(c).map((e) => [f(e), e])), s = new Map(w(t).map((e) => [f(e), e])), r = [...s].filter(([e]) => !d.has(e)).map(([, e]) => e), l = [...d].filter(([e]) => !s.has(e)).map(([, e]) => e), a = [], u = [];
5
5
  R(c, t, a, u);
@@ -131,4 +131,4 @@ function S(i, n) {
131
131
  export {
132
132
  q as d
133
133
  };
134
- //# sourceMappingURL=diff-CdZZG-N1.js.map
134
+ //# sourceMappingURL=diff-BZZNZs76.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"diff-CdZZG-N1.js","sources":["../src/analyze/diff.ts"],"sourcesContent":["// S7: one SDL, two diffs. A change to the annotated SDL changes both the\n// database (constraints, indexes, labels, property names) and the public\n// API; `diffSchemas` reports both from the same two documents.\n\nimport * as graphql from \"graphql\";\nimport type { DocumentNode, GraphQLSchema } from \"graphql\";\nimport { name } from \"../compile/cypher.js\";\nimport { compareConstraints } from \"../model/constraints.js\";\nimport { buildModel } from \"../model/build.js\";\nimport { type ModelOptions } from \"../model/build/limits.js\";\nimport type { Field, GraphModel, ScalarField } from \"../model/types.js\";\nimport { buildSchema } from \"../schema/build.js\";\nimport {\n inferRequirements,\n requirementDdl,\n requirementId,\n type SchemaRequirement,\n} from \"./indexes.js\";\n\nexport interface ApiChange {\n /** graphql-js change type, e.g. `FIELD_REMOVED`. */\n type: string;\n description: string;\n}\n\nexport interface SchemaDiff {\n database: {\n create: SchemaRequirement[];\n drop: SchemaRequirement[];\n /**\n * Statements in the order to run them. Drops and data migrations are\n * destructive: review them before running.\n */\n statements: Array<{ text: string; destructive: boolean; reason: string }>;\n };\n api: {\n /** Changes that break existing clients. */\n breaking: ApiChange[];\n /** Changes that may change behaviour for existing clients. */\n dangerous: ApiChange[];\n };\n /** Consequences worth a human look, e.g. a rename kept by @alias. */\n notes: string[];\n}\n\nexport function diffSchemas(\n before: string | DocumentNode,\n after: string | DocumentNode,\n options: ModelOptions = {},\n): SchemaDiff {\n const oldModel = buildModel(before, options);\n const newModel = buildModel(after, options);\n\n const id = requirementId;\n const oldReqs = new Map(inferRequirements(oldModel).map((r) => [id(r), r]));\n const newReqs = new Map(inferRequirements(newModel).map((r) => [id(r), r]));\n const create = [...newReqs]\n .filter(([k]) => !oldReqs.has(k))\n .map(([, r]) => r);\n const drop = [...oldReqs].filter(([k]) => !newReqs.has(k)).map(([, r]) => r);\n\n const statements: SchemaDiff[\"database\"][\"statements\"] = [];\n const notes: string[] = [];\n migrations(oldModel, newModel, statements, notes);\n for (const r of drop) {\n statements.push({\n text: `DROP ${r.kind === \"constraint\" ? \"CONSTRAINT\" : \"INDEX\"} \\`${r.name.replace(/`/g, \"``\")}\\` IF EXISTS`,\n destructive: true,\n reason: `no longer needed: ${r.reason}`,\n });\n }\n for (const r of create) {\n statements.push({\n text: requirementDdl(r),\n destructive: false,\n reason: r.reason,\n });\n }\n\n const api = apiChanges(publicSchema(oldModel), publicSchema(newModel));\n ruleChanges(oldModel, newModel, api, notes);\n return { database: { create, drop, statements }, api, notes };\n}\n\n/** Relabelled types and renamed properties need their data moved. */\nfunction migrations(\n before: GraphModel,\n after: GraphModel,\n statements: SchemaDiff[\"database\"][\"statements\"],\n notes: string[],\n) {\n for (const [typeName, oldNode] of before.nodes) {\n const newNode = after.nodes.get(typeName);\n if (!newNode) {\n notes.push(\n `${typeName} was removed; its :${oldNode.labels[0]} nodes stay in the database`,\n );\n continue;\n }\n const oldLabel = oldNode.labels[0]!;\n const newLabel = newNode.labels[0]!;\n if (oldLabel !== newLabel) {\n statements.push({\n text: `MATCH (n:${name(oldLabel)}) SET n:${name(newLabel)} REMOVE n:${name(oldLabel)}`,\n destructive: true,\n reason: `${typeName}'s label changed from ${oldLabel} to ${newLabel}`,\n });\n }\n const oldFields = scalars(oldNode.fields);\n const newFields = scalars(newNode.fields);\n for (const [fieldName, oldField] of oldFields) {\n const newField = newFields.get(fieldName);\n if (newField && newField.property !== oldField.property) {\n statements.push({\n text: `MATCH (n:${name(newLabel)}) WHERE n.${name(oldField.property)} IS NOT NULL SET n.${name(newField.property)} = n.${name(oldField.property)} REMOVE n.${name(oldField.property)}`,\n destructive: true,\n reason: `${typeName}.${fieldName} now stores \\`${newField.property}\\` instead of \\`${oldField.property}\\``,\n });\n }\n if (!newField) {\n const renamed = [...newFields.values()].find(\n (f) => f.property === oldField.property && !oldFields.has(f.name),\n );\n if (renamed) {\n notes.push(\n `${typeName}.${fieldName} is now ${typeName}.${renamed.name} over the same property \\`${oldField.property}\\`: an API break with no data migration`,\n );\n }\n }\n }\n }\n}\n\n/**\n * Validation rules are not part of the GraphQL type system, so graphql-js\n * sees no change when one is tightened: a `String` is still a `String`.\n * Compared here, on the model: a rule that now refuses input it accepted\n * breaks clients that send it.\n */\nfunction ruleChanges(\n before: GraphModel,\n after: GraphModel,\n api: SchemaDiff[\"api\"],\n notes: string[],\n) {\n const owners = (model: GraphModel) =>\n new Map<string, ReadonlyMap<string, Field | ScalarField>>([\n ...[...model.nodes].map(([n, t]) => [n, t.fields] as const),\n ...[...model.relationshipProperties].map(\n ([n, t]) => [n, t.fields] as const,\n ),\n ]);\n const old = owners(before);\n let stricter = 0;\n for (const [owner, fields] of owners(after)) {\n const oldFields = old.get(owner);\n if (!oldFields) continue;\n for (const f of fields.values()) {\n const was = oldFields.get(f.name);\n if (f.kind !== \"scalar\" || was?.kind !== \"scalar\") continue;\n const { tightened, changed } = compareConstraints(\n was.constraints,\n f.constraints,\n );\n for (const what of tightened) {\n stricter++;\n api.breaking.push({\n type: \"RULE_TIGHTENED\",\n description: `${owner}.${f.name}: ${what}. Input the old rule accepted is now refused.`,\n });\n }\n for (const what of changed) {\n stricter++;\n api.dangerous.push({\n type: \"RULE_CHANGED\",\n description: `${owner}.${f.name}: ${what}.`,\n });\n }\n }\n }\n if (stricter > 0) {\n notes.push(\n `${stricter === 1 ? \"A validation rule was\" : `${stricter} validation rules were`} added, tightened or changed: values already stored are not revisited. \\`lora-graphql check --database\\` lists the ones that no longer pass.`,\n );\n }\n}\n\nfunction scalars(fields: ReadonlyMap<string, Field>): Map<string, ScalarField> {\n const out = new Map<string, ScalarField>();\n for (const f of fields.values()) if (f.kind === \"scalar\") out.set(f.name, f);\n return out;\n}\n\nfunction publicSchema(model: GraphModel): GraphQLSchema {\n const never = () => Promise.reject(new Error(\"not connected\"));\n return buildSchema(model, {\n customResolver: () => () => null,\n resolveSearch: never,\n resolveAbstract: never,\n subscribe: () => {\n throw new Error(\"not connected\");\n },\n resolveChangedNode: never,\n previousValue: () => null,\n resolveRoot: never,\n resolveNode: never,\n resolveCypher: never,\n resolveMutation: never,\n });\n}\n\ntype ChangeFinder = (\n a: GraphQLSchema,\n b: GraphQLSchema,\n) => Array<{ type: string; description: string }>;\n\nfunction apiChanges(\n before: GraphQLSchema,\n after: GraphQLSchema,\n): SchemaDiff[\"api\"] {\n // graphql-js 16 ships these; 17 removed them.\n const lib = graphql as unknown as {\n findBreakingChanges?: ChangeFinder;\n findDangerousChanges?: ChangeFinder;\n };\n if (!lib.findBreakingChanges || !lib.findDangerousChanges) {\n return {\n breaking: [\n {\n type: \"UNSUPPORTED\",\n description:\n \"this graphql version has no findBreakingChanges; compare printPublicSchema() output with @graphql-inspector/core\",\n },\n ],\n dangerous: [],\n };\n }\n const pick = (c: { type: string; description: string }) => ({\n type: c.type,\n description: c.description,\n });\n return {\n breaking: lib.findBreakingChanges(before, after).map(pick),\n dangerous: lib.findDangerousChanges(before, after).map(pick),\n };\n}\n"],"names":["diffSchemas","before","after","options","oldModel","buildModel","newModel","id","requirementId","oldReqs","inferRequirements","r","newReqs","create","k","drop","statements","notes","migrations","requirementDdl","api","apiChanges","publicSchema","ruleChanges","typeName","oldNode","newNode","oldLabel","newLabel","name","oldFields","scalars","newFields","fieldName","oldField","newField","renamed","f","owners","model","n","t","old","stricter","owner","fields","was","tightened","changed","compareConstraints","what","out","never","buildSchema","lib","graphql","pick","c"],"mappings":"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"}
1
+ {"version":3,"file":"diff-BZZNZs76.js","sources":["../src/analyze/diff.ts"],"sourcesContent":["// S7: one SDL, two diffs. A change to the annotated SDL changes both the\n// database (constraints, indexes, labels, property names) and the public\n// API; `diffSchemas` reports both from the same two documents.\n\nimport * as graphql from \"graphql\";\nimport type { DocumentNode, GraphQLSchema } from \"graphql\";\nimport { name } from \"../compile/cypher.js\";\nimport { compareConstraints } from \"../model/constraints.js\";\nimport { buildModel } from \"../model/build.js\";\nimport { type ModelOptions } from \"../model/build/limits.js\";\nimport type { Field, GraphModel, ScalarField } from \"../model/types.js\";\nimport { buildSchema } from \"../schema/build.js\";\nimport {\n inferRequirements,\n requirementDdl,\n requirementId,\n type SchemaRequirement,\n} from \"./indexes.js\";\n\nexport interface ApiChange {\n /** graphql-js change type, e.g. `FIELD_REMOVED`. */\n type: string;\n description: string;\n}\n\nexport interface SchemaDiff {\n database: {\n create: SchemaRequirement[];\n drop: SchemaRequirement[];\n /**\n * Statements in the order to run them. Drops and data migrations are\n * destructive: review them before running.\n */\n statements: Array<{ text: string; destructive: boolean; reason: string }>;\n };\n api: {\n /** Changes that break existing clients. */\n breaking: ApiChange[];\n /** Changes that may change behaviour for existing clients. */\n dangerous: ApiChange[];\n };\n /** Consequences worth a human look, e.g. a rename kept by @alias. */\n notes: string[];\n}\n\nexport function diffSchemas(\n before: string | DocumentNode,\n after: string | DocumentNode,\n options: ModelOptions = {},\n): SchemaDiff {\n const oldModel = buildModel(before, options);\n const newModel = buildModel(after, options);\n\n const id = requirementId;\n const oldReqs = new Map(inferRequirements(oldModel).map((r) => [id(r), r]));\n const newReqs = new Map(inferRequirements(newModel).map((r) => [id(r), r]));\n const create = [...newReqs]\n .filter(([k]) => !oldReqs.has(k))\n .map(([, r]) => r);\n const drop = [...oldReqs].filter(([k]) => !newReqs.has(k)).map(([, r]) => r);\n\n const statements: SchemaDiff[\"database\"][\"statements\"] = [];\n const notes: string[] = [];\n migrations(oldModel, newModel, statements, notes);\n for (const r of drop) {\n statements.push({\n text: `DROP ${r.kind === \"constraint\" ? \"CONSTRAINT\" : \"INDEX\"} \\`${r.name.replace(/`/g, \"``\")}\\` IF EXISTS`,\n destructive: true,\n reason: `no longer needed: ${r.reason}`,\n });\n }\n for (const r of create) {\n statements.push({\n text: requirementDdl(r),\n destructive: false,\n reason: r.reason,\n });\n }\n\n const api = apiChanges(publicSchema(oldModel), publicSchema(newModel));\n ruleChanges(oldModel, newModel, api, notes);\n return { database: { create, drop, statements }, api, notes };\n}\n\n/** Relabelled types and renamed properties need their data moved. */\nfunction migrations(\n before: GraphModel,\n after: GraphModel,\n statements: SchemaDiff[\"database\"][\"statements\"],\n notes: string[],\n) {\n for (const [typeName, oldNode] of before.nodes) {\n const newNode = after.nodes.get(typeName);\n if (!newNode) {\n notes.push(\n `${typeName} was removed; its :${oldNode.labels[0]} nodes stay in the database`,\n );\n continue;\n }\n const oldLabel = oldNode.labels[0]!;\n const newLabel = newNode.labels[0]!;\n if (oldLabel !== newLabel) {\n statements.push({\n text: `MATCH (n:${name(oldLabel)}) SET n:${name(newLabel)} REMOVE n:${name(oldLabel)}`,\n destructive: true,\n reason: `${typeName}'s label changed from ${oldLabel} to ${newLabel}`,\n });\n }\n const oldFields = scalars(oldNode.fields);\n const newFields = scalars(newNode.fields);\n for (const [fieldName, oldField] of oldFields) {\n const newField = newFields.get(fieldName);\n if (newField && newField.property !== oldField.property) {\n statements.push({\n text: `MATCH (n:${name(newLabel)}) WHERE n.${name(oldField.property)} IS NOT NULL SET n.${name(newField.property)} = n.${name(oldField.property)} REMOVE n.${name(oldField.property)}`,\n destructive: true,\n reason: `${typeName}.${fieldName} now stores \\`${newField.property}\\` instead of \\`${oldField.property}\\``,\n });\n }\n if (!newField) {\n const renamed = [...newFields.values()].find(\n (f) => f.property === oldField.property && !oldFields.has(f.name),\n );\n if (renamed) {\n notes.push(\n `${typeName}.${fieldName} is now ${typeName}.${renamed.name} over the same property \\`${oldField.property}\\`: an API break with no data migration`,\n );\n }\n }\n }\n }\n}\n\n/**\n * Validation rules are not part of the GraphQL type system, so graphql-js\n * sees no change when one is tightened: a `String` is still a `String`.\n * Compared here, on the model: a rule that now refuses input it accepted\n * breaks clients that send it.\n */\nfunction ruleChanges(\n before: GraphModel,\n after: GraphModel,\n api: SchemaDiff[\"api\"],\n notes: string[],\n) {\n const owners = (model: GraphModel) =>\n new Map<string, ReadonlyMap<string, Field | ScalarField>>([\n ...[...model.nodes].map(([n, t]) => [n, t.fields] as const),\n ...[...model.relationshipProperties].map(\n ([n, t]) => [n, t.fields] as const,\n ),\n ]);\n const old = owners(before);\n let stricter = 0;\n for (const [owner, fields] of owners(after)) {\n const oldFields = old.get(owner);\n if (!oldFields) continue;\n for (const f of fields.values()) {\n const was = oldFields.get(f.name);\n if (f.kind !== \"scalar\" || was?.kind !== \"scalar\") continue;\n const { tightened, changed } = compareConstraints(\n was.constraints,\n f.constraints,\n );\n for (const what of tightened) {\n stricter++;\n api.breaking.push({\n type: \"RULE_TIGHTENED\",\n description: `${owner}.${f.name}: ${what}. Input the old rule accepted is now refused.`,\n });\n }\n for (const what of changed) {\n stricter++;\n api.dangerous.push({\n type: \"RULE_CHANGED\",\n description: `${owner}.${f.name}: ${what}.`,\n });\n }\n }\n }\n if (stricter > 0) {\n notes.push(\n `${stricter === 1 ? \"A validation rule was\" : `${stricter} validation rules were`} added, tightened or changed: values already stored are not revisited. \\`lora-graphql check --database\\` lists the ones that no longer pass.`,\n );\n }\n}\n\nfunction scalars(fields: ReadonlyMap<string, Field>): Map<string, ScalarField> {\n const out = new Map<string, ScalarField>();\n for (const f of fields.values()) if (f.kind === \"scalar\") out.set(f.name, f);\n return out;\n}\n\nfunction publicSchema(model: GraphModel): GraphQLSchema {\n const never = () => Promise.reject(new Error(\"not connected\"));\n return buildSchema(model, {\n customResolver: () => () => null,\n resolveSearch: never,\n resolveAbstract: never,\n subscribe: () => {\n throw new Error(\"not connected\");\n },\n resolveChangedNode: never,\n previousValue: () => null,\n resolveRoot: never,\n resolveNode: never,\n resolveCypher: never,\n resolveMutation: never,\n });\n}\n\ntype ChangeFinder = (\n a: GraphQLSchema,\n b: GraphQLSchema,\n) => Array<{ type: string; description: string }>;\n\nfunction apiChanges(\n before: GraphQLSchema,\n after: GraphQLSchema,\n): SchemaDiff[\"api\"] {\n // graphql-js 16 ships these; 17 removed them.\n const lib = graphql as unknown as {\n findBreakingChanges?: ChangeFinder;\n findDangerousChanges?: ChangeFinder;\n };\n if (!lib.findBreakingChanges || !lib.findDangerousChanges) {\n return {\n breaking: [\n {\n type: \"UNSUPPORTED\",\n description:\n \"this graphql version has no findBreakingChanges; compare printPublicSchema() output with @graphql-inspector/core\",\n },\n ],\n dangerous: [],\n };\n }\n const pick = (c: { type: string; description: string }) => ({\n type: c.type,\n description: c.description,\n });\n return {\n breaking: lib.findBreakingChanges(before, after).map(pick),\n dangerous: lib.findDangerousChanges(before, after).map(pick),\n };\n}\n"],"names":["diffSchemas","before","after","options","oldModel","buildModel","newModel","id","requirementId","oldReqs","inferRequirements","r","newReqs","create","k","drop","statements","notes","migrations","requirementDdl","api","apiChanges","publicSchema","ruleChanges","typeName","oldNode","newNode","oldLabel","newLabel","name","oldFields","scalars","newFields","fieldName","oldField","newField","renamed","f","owners","model","n","t","old","stricter","owner","fields","was","tightened","changed","compareConstraints","what","out","never","buildSchema","lib","graphql","pick","c"],"mappings":"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"}
@@ -50,6 +50,14 @@ export interface WriteChange {
50
50
  */
51
51
  broad: boolean;
52
52
  }
53
+ /**
54
+ * Complete `change` with the relationships a mutation wrote: both lists,
55
+ * and the entities and types that follow from everything in it.
56
+ */
57
+ export declare function fillChange(change: WriteChange, wrote: {
58
+ connected: readonly RelationshipRef[];
59
+ disconnected: readonly RelationshipRef[];
60
+ }): void;
53
61
  export interface ReadSetLike {
54
62
  labels: readonly string[];
55
63
  relationships: readonly string[];
@@ -2,6 +2,10 @@ import { FieldNode } from 'graphql';
2
2
  import { Statement } from '../driver.js';
3
3
  import { CypherField } from '../model/types.js';
4
4
  import { MutationEnv } from './mutate/env.js';
5
+ import { Reported } from './mutate/reported.js';
5
6
  export declare function executeCypherMutation(env: MutationEnv, field: CypherField, args: Record<string, unknown>, fieldNodes: readonly FieldNode[],
6
7
  /** The field's `viewer` rules, checked in the transaction first. */
7
- guard?: Statement): Promise<unknown>;
8
+ guard?: Statement): Promise<{
9
+ value: unknown;
10
+ written: Reported;
11
+ }>;
@@ -0,0 +1,26 @@
1
+ import { CompileContext } from '../../compile/context.js';
2
+ import { QueryResult, Statement } from '../../driver.js';
3
+ import { GraphModel } from '../../model/types.js';
4
+ import { EntityRef } from '../changes.js';
5
+ export interface Reported {
6
+ created: EntityRef[];
7
+ updated: EntityRef[];
8
+ /** The statement wrote something listeners cannot be told about. */
9
+ broad: boolean;
10
+ }
11
+ export declare const refId: (e: EntityRef) => string;
12
+ /**
13
+ * The nodes `result` returns as `created` or `updated`, each once (a node
14
+ * in both is created). A statement that writes and names neither is
15
+ * broad, and so is one that returns something that is not a node of the
16
+ * model (a key, a node of an unknown label).
17
+ */
18
+ export declare function reportedWrites(model: GraphModel, statement: {
19
+ writes: boolean;
20
+ reports: boolean;
21
+ }, result: QueryResult): Reported;
22
+ /**
23
+ * A statement's CREATE knows nothing of @outbox: the nodes it reports as
24
+ * created become work with these, in the same transaction.
25
+ */
26
+ export declare function outboxStatements(model: GraphModel, created: readonly EntityRef[], ctx: () => CompileContext): Statement[];
@@ -14,6 +14,12 @@ export declare class Runner {
14
14
  info: MutationInfo;
15
15
  connected: RelationshipRef[];
16
16
  disconnected: RelationshipRef[];
17
+ /**
18
+ * The relationships of `connected` that were not there before, for
19
+ * CONNECT triggers. Filled in a transaction, which is where a mutation
20
+ * of a schema with triggers runs.
21
+ */
22
+ linked: RelationshipRef[];
17
23
  /** Nodes updated per type, for AFTER validation. */
18
24
  updated: Map<NodeType, {
19
25
  keys: unknown[];
@@ -1,6 +1,6 @@
1
1
  import { CompileContext } from '../../compile/context.js';
2
2
  import { Statement } from '../../driver.js';
3
- import { MutationOperation, NodeType, Trigger } from '../../model/types.js';
3
+ import { NodeType, Trigger, TriggerOperation } from '../../model/types.js';
4
4
  import { MutationEnv } from './env.js';
5
5
  import { Runner } from './runner.js';
6
6
  /** Whether the mutation may fire a trigger: a statement or a function. */
@@ -8,7 +8,13 @@ export declare function hasTriggers(env: MutationEnv): boolean;
8
8
  /** Whether the mutation may call a trigger function. */
9
9
  export declare function hasTriggerFunctions(env: MutationEnv): boolean;
10
10
  /** The trigger statement for `keys`, with `this` bound to each node. */
11
- export declare function triggerStatement(ctx: CompileContext, node: NodeType, trigger: Trigger, operation: MutationOperation, keys: unknown[]): Statement;
11
+ export declare function triggerStatement(ctx: CompileContext, node: NodeType, trigger: Trigger, operation: TriggerOperation, keys: unknown[]): Statement;
12
+ /**
13
+ * The text ahead of a CONNECT or DISCONNECT trigger's statement: one row
14
+ * per pair of `pairs` (a list of `[this key, target key]`), with `this`
15
+ * and `target` bound.
16
+ */
17
+ export declare function relationshipTriggerPrefix(node: NodeType, target: NodeType, pairs: string): string;
12
18
  /**
13
19
  * Run the CREATE and UPDATE triggers for what the mutation wrote so far.
14
20
  * A node the mutation created and then updated fires CREATE only.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { D as r, L as e, a as o, b as i, M as l, c as n, d, e as t, f as m, g as p, h as c, i as f, j as L, k as h, l as R, p as D, r as u, s as E, m as G, t as b, v } from "./lora-graphql-D11QFUkf.js";
2
- import { d as O } from "./diff-CdZZG-N1.js";
1
+ import { D as r, L as e, a as o, b as i, M as l, c as n, d, e as t, f as m, g as p, h as c, i as f, j as L, k as h, l as R, p as D, r as u, s as E, m as G, t as b, v } from "./lora-graphql-RvZQdHNA.js";
2
+ import { d as O } from "./diff-BZZNZs76.js";
3
3
  export {
4
4
  r as DEFAULT_GUARDS,
5
5
  e as LORA_GRAPHQL_ERROR_CODES,