@loradb/lora-graphql 0.17.0 → 0.19.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.
package/README.md CHANGED
@@ -389,7 +389,14 @@ mutation {
389
389
  is an error that writes nothing, and an empty `where` is refused.
390
390
  - **`upsertFestivals`** creates the inputs whose key is new and updates the
391
391
  rest; fields required on create are required only for new keys. A key the
392
- caller may not see reads as taken.
392
+ caller may not see is never updated, and never reported as taken: it gets
393
+ the answer a free key would (see below).
394
+ - **Creating under a hidden key** (`createFestivals`, `upsertFestivals`, a
395
+ nested `create`) answers as if the key were free: whatever error the free
396
+ key would get, and where it would be created, `FORBIDDEN` ("not allowed to
397
+ create"), never a `CONSTRAINT_VIOLATION` that would confirm the key
398
+ exists. A key held by a node the caller can read is still reported as
399
+ taken. See `docs/design/graphql-threat-model.md`.
393
400
  - **Deletes** follow `onDelete`: `DETACH` (default) removes the
394
401
  relationships, `CASCADE` deletes what the field reaches (checking the
395
402
  caller may delete each node), `RESTRICT` refuses while related nodes
@@ -598,7 +605,7 @@ type Post
598
605
  with their `sub` and `:`. The claim must be a string, number or boolean;
599
606
  otherwise the rule denies. Pick a separator no `sub` contains: with `-`,
600
607
  user `a` could take `a-b-…`, the key space of user `a-b`. See
601
- [docs/design/graphql-claim-interpolation.md](../../docs/design/graphql-claim-interpolation.md).
608
+ [docs/design/graphql-threat-model.md](../../docs/design/graphql-threat-model.md).
602
609
  - **Claim tests run in JavaScript at compile time**, so an admin's
603
610
  statement carries no filter at all. Statements stay specialised and
604
611
  index-friendly.
@@ -620,8 +627,12 @@ type Post
620
627
  - Field-level `@authorization(validate:)` guards one field: reading it on a
621
628
  row that fails is `FORBIDDEN`, filtering by it only matches rows that
622
629
  pass, sorting or aggregating by it is refused, and writing it checks the
623
- rule. Field-level `@authentication` also guards filtering, sorting and
624
- aggregating on the field.
630
+ rule. A write is what the input sets: a create that leaves the field out
631
+ is not checked against it, even when `@default` or `@populatedBy` fills
632
+ it, so a CREATE rule can keep a `verified: Boolean! @default(value: false)`
633
+ settable by admins only while anyone creates the node. Field-level
634
+ `@authentication` also guards filtering, sorting and aggregating on the
635
+ field.
625
636
  - Reading a guarded field is checked per row, token or not: without the
626
637
  token a rule needs, each row reads the field as `UNAUTHENTICATED`. The
627
638
  statement has the same shape either way, so `compile()`, `explain()`,
@@ -693,7 +704,10 @@ operations; the CLI does it in CI.
693
704
  **S3: compile once.** `lora.persist({ id: source })` parses and validates
694
705
  persisted operations at startup, and `lora.execute({ id, variables, context })`
695
706
  runs them with no parsing or validation. Ad hoc documents passed to
696
- `execute({ source })` are cached too. Translation itself takes about 0.13 ms
707
+ `execute({ source })` are cached too. A subscription runs the same way with
708
+ `lora.subscribe({ id | source, variables, context })`, which returns an
709
+ async iterable of results (end it with `context.signal`); `execute()` answers
710
+ a subscription with an error pointing there. Translation itself takes about 0.13 ms
697
711
  for a nested page, and statement text depends only on the shape of the
698
712
  input, so LoraDB's own plan cache is hit for every repeat.
699
713
 
@@ -901,9 +915,12 @@ Both need `@loradb/lora-node` and work with any test runner.
901
915
  ## Drivers, limits and errors
902
916
 
903
917
  `loraDriver(db)` adapts a `Database` from `@loradb/lora-node` or
904
- `@loradb/lora-wasm`. Single-statement reads stream; multi-statement reads
905
- run in one read-only transaction; mutations need interactive transactions,
906
- which only the Node binding has, so the WASM binding serves reads.
918
+ `@loradb/lora-wasm`. A read of one statement runs with `execute()` (with
919
+ lora-node, on a libuv worker, not the JavaScript thread), except a lookup
920
+ by `@key` that selects only stored fields, which streams its single row
921
+ synchronously; a read of several statements runs in one read-only
922
+ transaction. Mutations need interactive transactions, which only the Node
923
+ binding has, so the WASM binding serves reads.
907
924
  `explain()`, and so plan checks, need the Node binding too.
908
925
 
909
926
  | Option | Default | Meaning |
@@ -920,7 +937,7 @@ which only the Node binding has, so the WASM binding serves reads.
920
937
  | `maskErrors` | production | Clients get `DATABASE_ERROR` and an `id` only |
921
938
  | `onError` | | Receives each database error's detail and `id` |
922
939
  | `guards` | see below | Document limits; `false` turns them off |
923
- | `persistedOnly` | false | `execute()` runs persisted operations only |
940
+ | `persistedOnly` | false | `execute()`, `subscribe()` run persisted ops only |
924
941
  | `budget` | | Cost limit per request, from the context |
925
942
  | `onCost` | | Each root field's estimate, total and limit |
926
943
  | `onStatementEnd` | | Duration, rows and error of every statement call |
@@ -930,21 +947,24 @@ which only the Node binding has, so the WASM binding serves reads.
930
947
  Errors carry `extensions.code`: `BAD_USER_INPUT`, `INVALID_CURSOR`,
931
948
  `LIMIT_EXCEEDED`, `COST_EXCEEDED`, `UNAUTHENTICATED`, `FORBIDDEN`,
932
949
  `NOT_FOUND`, `CONSTRAINT_VIOLATION` (with `type` and `field`),
933
- `DATABASE_ERROR` (with an `id`, also given to `onError`) and
934
- `PERSISTED_QUERY_ONLY` (`execute()` got a document under `persistedOnly`). An invalid SDL
950
+ `DATABASE_ERROR` (with an `id`, also given to `onError`),
951
+ `PERSISTED_QUERY_ONLY` (`execute()` or `subscribe()` got a document under
952
+ `persistedOnly`) and `WRONG_OPERATION_TYPE` (`execute()` got a
953
+ subscription, or `subscribe()` a query or mutation). An invalid SDL
935
954
  throws one `ModelError` listing every problem, each located by type and
936
955
  field.
937
956
 
938
957
  ### Security defaults
939
958
 
940
959
  `maxCost` bounds the rows an operation touches; the document guards bound
941
- the document before that. `execute()` and `persist()` apply them, and
960
+ the document before that. `execute()`, `subscribe()` and `persist()` apply them, and
942
961
  `lora.validationRules()` / `lora.envelopPlugin()` bring them to any other
943
962
  server (GraphQL Yoga takes the plugin as is):
944
963
 
945
964
  | Guard | Default | Limit |
946
965
  | --------------- | ---------- | ---------------------------------------- |
947
966
  | `maxDepth` | 12 | Field nesting, through fragments |
967
+ | `maxIntrospectionDepth` | 20 | Nesting under `__schema` / `__type` |
948
968
  | `maxAliases` | 30 | Aliased fields per document |
949
969
  | `maxRootFields` | 20 | Root fields per operation |
950
970
  | `maxTokens` | 5000 | Lexer tokens per document, while parsing |
@@ -1003,7 +1023,7 @@ Measured on LoraDB 0.15 over 20 000 festivals and 100 000 relationships
1003
1023
 
1004
1024
  | Rule | Why |
1005
1025
  | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
1006
- | Relationship filters as `size([… \| 1])`; aggregates of related values with `reduce` | No `OPTIONAL MATCH`; `EXISTS { }` does not parse; aggregates nest safely |
1026
+ | Relationship filters as `size([… \| 1])`; aggregates of related values with `reduce` | No `OPTIONAL MATCH`; aggregates nest safely |
1007
1027
  | `CALL { }` for nested lists, nested connections, `@cypher` and interface members | The only way to sort, limit and aggregate per parent |
1008
1028
  | Ordered by an always-present string: `WHERE s >= ""` | The planner walks the index in order and stops at the limit: 0.03 ms instead of 7 ms |
1009
1029
  | Mutation statements seek the key in their own `MATCH`, then expand | The plan no longer depends on the optimizer finding the seek: 0.06 ms per connect |
@@ -1028,7 +1048,7 @@ Measured on LoraDB 0.15 over 20 000 festivals and 100 000 relationships
1028
1048
  | `@authorization` rules evaluated in Cypher | Claim checks folded in JavaScript; node rules compiled |
1029
1049
  | You pick indexes | Inferred from the API, and verified with `explain()` |
1030
1050
  | Unbounded lists; complexity left to you | Every list bounded; a cost limit per operation |
1031
- | Subscriptions over CDC | `@subscription`, from library-made writes |
1051
+ | Subscriptions over CDC | `@subscription`, from library-made writes or, with `changeFeed: true`, every committed write |
1032
1052
  | Interfaces and unions with `UNION` | Per-member subqueries merged by sort, each using its own indexes |
1033
1053
  | Federation | Not supported |
1034
1054
 
@@ -1048,7 +1068,7 @@ before a following `SET`, list comparison (`[a, b] > $list`) and
1048
1068
  | An aggregate nested in a call (`head(collect(x))`, `collect(x)[0..2]`) is not aggregated | `WITH collect(x) AS c RETURN head(c)`; `reduce` for per-parent aggregates |
1049
1069
  | `max`, `sum` and `avg` over durations are wrong | Duration aggregates are folded with `reduce` |
1050
1070
  | Integer division returns a float; negative list slices (`l[..-1]`) return `[]` | `toInteger(a / b)` for Int fields; `l[..size(l) - n]` |
1051
- | `COUNT { … RETURN DISTINCT x }`, `EXISTS { }` and `UNION` inside `CALL` do not parse | `reduce` for distinct counts; comprehensions; per-member subqueries |
1071
+ | `COUNT { … RETURN DISTINCT x }` and `UNION` inside `CALL` do not parse | `reduce` for distinct counts; per-member subqueries |
1052
1072
 
1053
1073
  ## Development
1054
1074
 
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
  import { stat as z, readdir as oe, readFile as ie, mkdir as re, writeFile as _ } from "node:fs/promises";
3
3
  import { join as $, relative as ce } from "node:path";
4
4
  import { parse as q, Kind as p, visit as H, print as Q, typeFromAST as ue, isNonNullType as F, isListType as le, getNamedType as fe, isEnumType as de, isInputObjectType as pe } from "graphql";
5
- import { d as me } from "./diff-8nsBSSNP.js";
6
- import { m as V, d as he, M as ge, L as M, l as Y, f as J, o as Z, b as ve, i as we, r as Te, u as Ee } from "./driver-BmnRtkpd.js";
5
+ import { d as me } from "./diff-BH4Na9pC.js";
6
+ import { m as V, d as he, M as ge, L as M, l as Y, f as J, o as Z, b as ve, i as we, r as Te, u as Ee } from "./driver-CuXOoYWT.js";
7
7
  const be = /* @__PURE__ */ new Set(["Query", "Mutation", "Subscription"]), W = {
8
8
  coalesce: "@coalesce is not supported: it hides predicates from indexes; store a value instead",
9
9
  subscriptionsAuthorization: "@subscriptionsAuthorization is not supported: SUBSCRIBE rules go in @authorization",
@@ -27,6 +27,12 @@ export interface CompiledRead {
27
27
  cost: number;
28
28
  /** `read` for queries; `write` for @cypher mutations. */
29
29
  mode: "read" | "write";
30
+ /**
31
+ * At most one row, found by an exact @key seek, projecting only stored
32
+ * properties: no relationship, @cypher field or rule traversal. Cheap
33
+ * enough for a driver to run synchronously.
34
+ */
35
+ bounded?: boolean;
30
36
  }
31
37
  type Args = Record<string, unknown>;
32
38
  /** A connection value before its resolvers turn it into edges/pageInfo. */
@@ -1,5 +1,5 @@
1
1
  import * as w from "graphql";
2
- import { b as $, i as b, r as E, j as N, n as i, k as S } from "./driver-BmnRtkpd.js";
2
+ import { b as $, i as b, r as E, j as N, n as i, k as S } from "./driver-CuXOoYWT.js";
3
3
  function k(t, n, o = {}) {
4
4
  const s = $(t, o), r = $(n, o), l = N, u = new Map(b(s).map((e) => [l(e), e])), c = new Map(b(r).map((e) => [l(e), e])), p = [...c].filter(([e]) => !u.has(e)).map(([, e]) => e), h = [...u].filter(([e]) => !c.has(e)).map(([, e]) => e), d = [], f = [];
5
5
  C(s, r, d, f);
@@ -96,4 +96,4 @@ function M(t, n) {
96
96
  export {
97
97
  k as d
98
98
  };
99
- //# sourceMappingURL=diff-8nsBSSNP.js.map
99
+ //# sourceMappingURL=diff-BH4Na9pC.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"diff-8nsBSSNP.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 { buildModel, type ModelOptions } from \"../model/build.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 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\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 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","typeName","oldNode","newNode","oldLabel","newLabel","name","oldFields","scalars","newFields","fieldName","oldField","newField","renamed","f","fields","out","model","never","buildSchema","lib","graphql","pick","c"],"mappings":"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"}
1
+ {"version":3,"file":"diff-BH4Na9pC.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 { buildModel, type ModelOptions } from \"../model/build.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 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\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 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","typeName","oldNode","newNode","oldLabel","newLabel","name","oldFields","scalars","newFields","fieldName","oldField","newField","renamed","f","fields","out","model","never","buildSchema","lib","graphql","pick","c"],"mappings":"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"}