@kubun/graphql 0.12.0 → 0.13.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.
@@ -1,3 +1,4 @@
1
1
  import { GraphQLScalarType } from 'graphql';
2
2
  export declare const GraphQLAttachmentID: GraphQLScalarType<string, string>;
3
+ export declare const GraphQLCredentialEntryID: GraphQLScalarType<string, string>;
3
4
  export declare const GraphQLDocID: GraphQLScalarType<string, string>;
@@ -1,4 +1,4 @@
1
- import { AttachmentID, DocumentID } from '@kubun/id';
1
+ import { AttachmentID, CredentialEntryID, DocumentID } from '@kubun/id';
2
2
  import { GraphQLError, GraphQLScalarType, Kind } from 'graphql';
3
3
  function validateAttachmentID(input) {
4
4
  if (typeof input !== 'string') {
@@ -20,6 +20,28 @@ export const GraphQLAttachmentID = new GraphQLScalarType({
20
20
  return validateAttachmentID(ast.value);
21
21
  }
22
22
  });
23
+ function validateCredentialEntryID(input) {
24
+ if (typeof input !== 'string') {
25
+ throw new GraphQLError(`Invalid CredentialEntryID input: ${input}`);
26
+ }
27
+ // The whole check. Every Kubun id matches the scalar's pattern, so a document
28
+ // id or a credential KEY id would otherwise reach a resolver that expects an
29
+ // entry and miss with a message about absence rather than about type.
30
+ CredentialEntryID.fromString(input);
31
+ return input;
32
+ }
33
+ export const GraphQLCredentialEntryID = new GraphQLScalarType({
34
+ name: 'CredentialEntryID',
35
+ description: 'A Kubun Credential Entry identifier',
36
+ serialize: validateCredentialEntryID,
37
+ parseValue: validateCredentialEntryID,
38
+ parseLiteral (ast) {
39
+ if (ast.kind !== Kind.STRING) {
40
+ throw new GraphQLError(`Can only validate strings as CredentialEntryID but got a: ${ast.kind}`);
41
+ }
42
+ return validateCredentialEntryID(ast.value);
43
+ }
44
+ });
23
45
  function validateDocID(input) {
24
46
  if (typeof input !== 'string') {
25
47
  throw new GraphQLError(`Invalid DocumentID input: ${input}`);
package/lib/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type { AccessWarning, AggregateScope, ComplexityStats, Context, CreateMutationParams, DocumentEventEmitter, DocumentSavedEvent, MutationContext, PluginContextMap, QueryContext, ReadContextDB, ReadContextParams, ReadOnlyContext, RemoveMutationParams, ScopedConnection, SetDocumentAccessOverrideParams, SetModelAccessDefaultsParams, SetMutationParams, SubscriptionContext, UpdateMutationParams, } from './context.js';
2
2
  export { createReadContext } from './context.js';
3
- export { GraphQLAttachmentID, GraphQLDocID } from './identifiers.js';
3
+ export { GraphQLAttachmentID, GraphQLCredentialEntryID, GraphQLDocID, } from './identifiers.js';
4
4
  export type { ExtensionResolvers, SchemaBuilderParams, SharedDefinitions, } from './schema.js';
5
5
  export { createSchema, SchemaBuilder } from './schema.js';
6
6
  export type * from './types.js';
package/lib/index.js CHANGED
@@ -4,5 +4,5 @@
4
4
  // list silently breaks resolver typing (`TS2339: Property '<name>' does not
5
5
  // exist on type 'Context'`).
6
6
  export { createReadContext } from './context.js';
7
- export { GraphQLAttachmentID, GraphQLDocID } from './identifiers.js';
7
+ export { GraphQLAttachmentID, GraphQLCredentialEntryID, GraphQLDocID } from './identifiers.js';
8
8
  export { createSchema, SchemaBuilder } from './schema.js';
package/lib/schema.d.ts CHANGED
@@ -194,6 +194,7 @@ export declare class SchemaBuilder {
194
194
  _buildArray(schema: ArrayModel, parentName?: string): GraphQLList<GraphQLNonNull<GraphQLOutputType>>;
195
195
  _buildObject(id: string, schema: ObjectModel, parentName?: string): GraphQLObjectType;
196
196
  _buildObjectFields(id: string, schema: DocumentModelSchema | ObjectModel, parentName?: string): GraphQLFieldConfigMap<Record<string, unknown>, Context>;
197
+ _buildNestedAttachmentField(key: string, fieldRef: string): GraphQLFieldConfig<Record<string, unknown>, Context> | null;
197
198
  _buildRelationFields(fields: GraphQLFieldConfigMap<DocumentNode, Context>, fieldName: string, relation: RelationField): void;
198
199
  _buildRelationSubscriptions(fields: GraphQLFieldConfigMap<unknown, Context>, modelID: string, fieldName: string, relation: RelationField): void;
199
200
  _buildString(schema: StringModel, parentName: string): GraphQLEnumType | GraphQLObjectType | GraphQLScalarType;
package/lib/schema.js CHANGED
@@ -4,7 +4,7 @@ import { pascalCase } from 'change-case';
4
4
  import { extendSchema, GraphQLBoolean, GraphQLEnumType, GraphQLFloat, GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString, isEnumType, isListType, isNonNullType, isObjectType, isScalarType, Kind, parse as parseGraphQL } from 'graphql';
5
5
  import { connectionArgs, connectionDefinitions, mutationWithClientMutationId, nodeDefinitions } from 'graphql-relay';
6
6
  import { GraphQLDateTimeISO, GraphQLDID, GraphQLDuration, GraphQLEmailAddress, GraphQLJSON, GraphQLJSONObject } from 'graphql-scalars';
7
- import { GraphQLAttachmentID, GraphQLDocID } from './identifiers.js';
7
+ import { GraphQLAttachmentID, GraphQLCredentialEntryID, GraphQLDocID } from './identifiers.js';
8
8
  import { GraphQLURL } from './scalars.js';
9
9
  import { getUniqueDocValue, validateAccessIdentifiers } from './utils.js';
10
10
  /**
@@ -44,9 +44,13 @@ const STRING_FORMATS = {
44
44
  };
45
45
  const STRING_TITLES = {
46
46
  attachmentid: GraphQLAttachmentID,
47
+ credentialentryid: GraphQLCredentialEntryID,
47
48
  did: GraphQLDID,
48
49
  docid: GraphQLDocID
49
50
  };
51
+ function isAttachmentIDString(schema) {
52
+ return schema.type === 'string' && schema.title?.toLowerCase() === 'attachmentid';
53
+ }
50
54
  function getReferenceID(path) {
51
55
  return path.slice(REFERENCE_PREFIX.length);
52
56
  }
@@ -105,7 +109,7 @@ function createEnumValueFilterInput(type) {
105
109
  }
106
110
  });
107
111
  }
108
- function createScalarValueFilterInput(type) {
112
+ function createScalarValueFilterInput(type, options = {}) {
109
113
  return new GraphQLInputObjectType({
110
114
  name: `${type.name}ValueFilter`,
111
115
  isOneOf: true,
@@ -136,6 +140,37 @@ function createScalarValueFilterInput(type) {
136
140
  },
137
141
  greaterThanOrEqualTo: {
138
142
  type
143
+ },
144
+ // Substring match only makes sense against string-backed scalars.
145
+ ...options.stringBacked ? {
146
+ contains: {
147
+ type: GraphQLString
148
+ }
149
+ } : {}
150
+ }
151
+ });
152
+ }
153
+ // Opaque identifiers (DID, DocumentID, AttachmentID): equality only, no ordering
154
+ // or substring match — the underlying strings are content-addressed, not text.
155
+ function createExactValueFilterInput(type) {
156
+ return new GraphQLInputObjectType({
157
+ name: `${type.name}ValueFilter`,
158
+ isOneOf: true,
159
+ fields: {
160
+ isNull: {
161
+ type: GraphQLBoolean
162
+ },
163
+ equalTo: {
164
+ type
165
+ },
166
+ notEqualTo: {
167
+ type
168
+ },
169
+ in: {
170
+ type: toList(type)
171
+ },
172
+ notIn: {
173
+ type: toList(type)
139
174
  }
140
175
  }
141
176
  });
@@ -159,6 +194,7 @@ const VALUE_FILTER_INPUTS = {
159
194
  }
160
195
  }
161
196
  }),
197
+ AttachmentIDValueFilter: createExactValueFilterInput(GraphQLAttachmentID),
162
198
  BooleanValueFilter: new GraphQLInputObjectType({
163
199
  name: 'BooleanValueFilter',
164
200
  isOneOf: true,
@@ -171,16 +207,26 @@ const VALUE_FILTER_INPUTS = {
171
207
  }
172
208
  }
173
209
  }),
210
+ DIDValueFilter: createExactValueFilterInput(GraphQLDID),
211
+ DocumentIDValueFilter: createExactValueFilterInput(GraphQLDocID),
174
212
  FloatValueFilter: createScalarValueFilterInput(GraphQLFloat),
175
213
  IntValueFilter: createScalarValueFilterInput(GraphQLInt),
176
- StringValueFilter: createScalarValueFilterInput(GraphQLString)
214
+ StringValueFilter: createScalarValueFilterInput(GraphQLString, {
215
+ stringBacked: true
216
+ })
177
217
  };
218
+ // Keyed by `scalar.name` (the GraphQL type name, e.g. "DID"), not the import
219
+ // binding — keying by binding name silently never matched here before, since
220
+ // scalars are looked up by `scalar.name`. `_owner` bypasses this map entirely
221
+ // (hardcoded to AccountValueFilter), so it is not listed here.
178
222
  const VALUE_FILTER_INPUT_TYPES = {
179
- GraphQLBoolean: 'BooleanValueFilter',
180
- GraphQLDID: 'AccountValueFilter',
181
- GraphQLFloat: 'FloatValueFilter',
182
- GraphQLInt: 'IntValueFilter',
183
- GraphQLString: 'StringValueFilter'
223
+ AttachmentID: 'AttachmentIDValueFilter',
224
+ Boolean: 'BooleanValueFilter',
225
+ DID: 'DIDValueFilter',
226
+ DocumentID: 'DocumentIDValueFilter',
227
+ Float: 'FloatValueFilter',
228
+ Int: 'IntValueFilter',
229
+ String: 'StringValueFilter'
184
230
  };
185
231
  const PatchSetOperation = new GraphQLInputObjectType({
186
232
  name: 'PatchSetOperation',
@@ -247,6 +293,26 @@ const OrderByDirection = new GraphQLEnumType({
247
293
  }
248
294
  }
249
295
  });
296
+ const ArrayPresence = new GraphQLEnumType({
297
+ name: 'ArrayPresence',
298
+ values: {
299
+ NULL: {
300
+ value: 'null'
301
+ },
302
+ NON_NULL: {
303
+ value: 'nonNull'
304
+ },
305
+ EMPTY: {
306
+ value: 'empty'
307
+ },
308
+ NON_EMPTY: {
309
+ value: 'nonEmpty'
310
+ },
311
+ NULL_OR_EMPTY: {
312
+ value: 'nullOrEmpty'
313
+ }
314
+ }
315
+ });
250
316
  const AccessRule = new GraphQLObjectType({
251
317
  name: 'AccessRule',
252
318
  fields: {
@@ -2161,16 +2227,34 @@ export class SchemaBuilder {
2161
2227
  if (schema.type === 'array') {
2162
2228
  const name = this._aliases[id] ?? pascalCase(parentName + schema.title);
2163
2229
  refParentName = name;
2164
- // const itemType = this._buildReferenceFilterInput(schema.items.$ref, schema.title)
2230
+ const itemID = getReferenceID(schema.items.$ref);
2231
+ const itemSchema = this._getReferenceSchema(itemID);
2232
+ // Membership ops need a scalar item type; array-of-object/array-of-array items
2233
+ // fall back to `isNull`-only filtering (no `_buildScalar` support for them).
2234
+ if (itemSchema.type === 'object' || itemSchema.type === 'array') {
2235
+ return new GraphQLInputObjectType({
2236
+ name: `${name}Filter`,
2237
+ isOneOf: true,
2238
+ fields: {
2239
+ isNull: {
2240
+ type: GraphQLBoolean
2241
+ }
2242
+ }
2243
+ });
2244
+ }
2245
+ const itemScalar = this._buildScalar(itemID, itemSchema, name);
2165
2246
  return new GraphQLInputObjectType({
2166
2247
  name: `${name}Filter`,
2167
2248
  isOneOf: true,
2168
2249
  fields: {
2169
- // DB does not support filtering on array values
2170
- // contains: { type: toList(itemType) },
2171
- // isEmpty: { type: GraphQLBoolean },
2172
- isNull: {
2173
- type: GraphQLBoolean
2250
+ includesAll: {
2251
+ type: toList(itemScalar)
2252
+ },
2253
+ includesAny: {
2254
+ type: toList(itemScalar)
2255
+ },
2256
+ presence: {
2257
+ type: ArrayPresence
2174
2258
  }
2175
2259
  }
2176
2260
  });
@@ -2180,7 +2264,9 @@ export class SchemaBuilder {
2180
2264
  if (knownFilter != null) {
2181
2265
  return this._inputObjects[knownFilter];
2182
2266
  }
2183
- const filter = isEnumType(scalar) ? createEnumValueFilterInput(scalar) : createScalarValueFilterInput(scalar);
2267
+ const filter = isEnumType(scalar) ? createEnumValueFilterInput(scalar) : createScalarValueFilterInput(scalar, {
2268
+ stringBacked: true
2269
+ });
2184
2270
  if (this._inputObjects[filter.name] == null) {
2185
2271
  this._inputObjects[filter.name] = filter;
2186
2272
  }
@@ -2357,6 +2443,11 @@ export class SchemaBuilder {
2357
2443
  if (fieldRef == null) {
2358
2444
  throw new Error(`Missing ref for field ${key} of object ${id}`);
2359
2445
  }
2446
+ const attachmentField = this._buildNestedAttachmentField(key, fieldRef);
2447
+ if (attachmentField != null) {
2448
+ fields[key] = attachmentField;
2449
+ continue;
2450
+ }
2360
2451
  const type = this._getReference(fieldRef, parentName);
2361
2452
  fields[key] = {
2362
2453
  type: required.includes(key) ? new GraphQLNonNull(type) : type
@@ -2364,6 +2455,24 @@ export class SchemaBuilder {
2364
2455
  }
2365
2456
  return fields;
2366
2457
  }
2458
+ // An attachmentid leaf nested inside an object is exposed as the AttachmentNode
2459
+ // object rather than a bare AttachmentID scalar, so state/mimeType/contentLength
2460
+ // resolve — mirroring the top-level attachment convenience fields. Always
2461
+ // nullable (like the top-level fields): the value may be absent, e.g. an
2462
+ // attachment not yet imported. Returns null for non-attachment fields.
2463
+ _buildNestedAttachmentField(key, fieldRef) {
2464
+ const refSchema = this._getReferenceSchema(getReferenceID(fieldRef));
2465
+ if (!isAttachmentIDString(refSchema)) {
2466
+ return null;
2467
+ }
2468
+ return {
2469
+ type: this._getDefinitions().attachmentObject,
2470
+ resolve: (source)=>{
2471
+ const value = source[key];
2472
+ return value ? AttachmentID.fromString(value) : null;
2473
+ }
2474
+ };
2475
+ }
2367
2476
  _buildRelationFields(fields, fieldName, relation) {
2368
2477
  if (relation.isList) {
2369
2478
  // DB does not support matches for array containing values
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubun/graphql",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "keywords": [],
5
5
  "license": "see LICENSE.md",
6
6
  "sideEffects": false,
@@ -22,20 +22,24 @@
22
22
  "change-case": "^5.4.4",
23
23
  "graphql": "^16.14.2",
24
24
  "graphql-relay": "^0.10.2",
25
- "graphql-scalars": "^1.25.0",
25
+ "graphql-scalars": "^2.0.0",
26
26
  "multiformats": "^14.0.5",
27
- "@kubun/id": "^0.12.0",
28
- "@kubun/logger": "^0.12.0",
29
- "@kubun/protocol": "^0.12.0",
30
- "@kubun/hlc": "^0.12.0"
27
+ "@kubun/hlc": "^0.13.0",
28
+ "@kubun/logger": "^0.13.0",
29
+ "@kubun/id": "^0.13.0",
30
+ "@kubun/protocol": "^0.13.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@kokuin/capability": "^0.2.1",
34
- "@kokuin/token": "^0.3.0",
35
- "@kubun/db": "^0.12.0",
36
- "@kubun/store-graph": "^0.12.0",
37
- "@kubun/test-utils": "^0.12.0",
38
- "@kubun/db-better-sqlite": "^0.12.0"
33
+ "@kokuin/capability": "^0.3.0",
34
+ "@kokuin/token": "^0.5.0",
35
+ "@kubun/db": "^0.13.0",
36
+ "@kubun/db-better-sqlite": "^0.13.0",
37
+ "@kubun/scalars": "^0.13.0",
38
+ "@kubun/store-graph": "^0.13.1",
39
+ "@kubun/test-utils": "^0.13.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
39
43
  },
40
44
  "scripts": {
41
45
  "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",