@contember/bindx 0.1.5 → 0.1.9

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 (76) hide show
  1. package/dist/adapter/ContemberAdapter.d.ts +11 -14
  2. package/dist/adapter/ContemberAdapter.d.ts.map +1 -1
  3. package/dist/brand/ComponentBrand.d.ts +1 -24
  4. package/dist/brand/ComponentBrand.d.ts.map +1 -1
  5. package/dist/contember/index.d.ts +1 -2
  6. package/dist/contember/index.d.ts.map +1 -1
  7. package/dist/handles/EntityHandle.d.ts +18 -29
  8. package/dist/handles/EntityHandle.d.ts.map +1 -1
  9. package/dist/handles/FieldHandle.d.ts +5 -3
  10. package/dist/handles/FieldHandle.d.ts.map +1 -1
  11. package/dist/handles/HasManyListHandle.d.ts +4 -3
  12. package/dist/handles/HasManyListHandle.d.ts.map +1 -1
  13. package/dist/handles/HasOneHandle.d.ts +33 -36
  14. package/dist/handles/HasOneHandle.d.ts.map +1 -1
  15. package/dist/handles/PlaceholderHandle.d.ts +9 -19
  16. package/dist/handles/PlaceholderHandle.d.ts.map +1 -1
  17. package/dist/handles/proxyFactory.d.ts +9 -37
  18. package/dist/handles/proxyFactory.d.ts.map +1 -1
  19. package/dist/handles/types.d.ts +4 -0
  20. package/dist/handles/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +3 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts +24 -1
  24. package/dist/persistence/ContemberSchemaMutationAdapter.d.ts.map +1 -1
  25. package/dist/schema/ContemberSchema.d.ts +1 -97
  26. package/dist/schema/ContemberSchema.d.ts.map +1 -1
  27. package/dist/schema/SchemaLoader.d.ts +1 -27
  28. package/dist/schema/SchemaLoader.d.ts.map +1 -1
  29. package/dist/schema/SchemaRegistry.d.ts +1 -94
  30. package/dist/schema/SchemaRegistry.d.ts.map +1 -1
  31. package/dist/schema/roles.d.ts +1 -61
  32. package/dist/schema/roles.d.ts.map +1 -1
  33. package/dist/schema/types.d.ts +1 -178
  34. package/dist/schema/types.d.ts.map +1 -1
  35. package/dist/selection/SelectionMetaCollector.d.ts +1 -31
  36. package/dist/selection/SelectionMetaCollector.d.ts.map +1 -1
  37. package/dist/selection/SelectionScope.d.ts +1 -76
  38. package/dist/selection/SelectionScope.d.ts.map +1 -1
  39. package/dist/selection/buildQuery.d.ts +1 -44
  40. package/dist/selection/buildQuery.d.ts.map +1 -1
  41. package/dist/selection/createFragment.d.ts +1 -28
  42. package/dist/selection/createFragment.d.ts.map +1 -1
  43. package/dist/selection/createSelectionBuilder.d.ts +1 -17
  44. package/dist/selection/createSelectionBuilder.d.ts.map +1 -1
  45. package/dist/selection/queryTypes.d.ts +1 -83
  46. package/dist/selection/queryTypes.d.ts.map +1 -1
  47. package/dist/selection/types.d.ts +1 -237
  48. package/dist/selection/types.d.ts.map +1 -1
  49. package/dist/utils/aliasGenerator.d.ts +1 -24
  50. package/dist/utils/aliasGenerator.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/adapter/ContemberAdapter.ts +193 -123
  53. package/src/brand/ComponentBrand.ts +1 -28
  54. package/src/contember/index.ts +1 -8
  55. package/src/handles/EntityHandle.ts +131 -61
  56. package/src/handles/FieldHandle.ts +19 -3
  57. package/src/handles/HasManyListHandle.ts +15 -12
  58. package/src/handles/HasOneHandle.ts +98 -66
  59. package/src/handles/PlaceholderHandle.ts +26 -24
  60. package/src/handles/proxyFactory.ts +29 -128
  61. package/src/handles/types.ts +6 -0
  62. package/src/index.ts +22 -3
  63. package/src/persistence/ContemberSchemaMutationAdapter.ts +19 -1
  64. package/src/schema/ContemberSchema.ts +14 -132
  65. package/src/schema/SchemaLoader.ts +1 -93
  66. package/src/schema/SchemaRegistry.ts +1 -286
  67. package/src/schema/roles.ts +10 -83
  68. package/src/schema/types.ts +21 -238
  69. package/src/selection/SelectionMetaCollector.ts +1 -122
  70. package/src/selection/SelectionScope.ts +1 -208
  71. package/src/selection/buildQuery.ts +1 -134
  72. package/src/selection/createFragment.ts +1 -50
  73. package/src/selection/createSelectionBuilder.ts +1 -283
  74. package/src/selection/queryTypes.ts +8 -135
  75. package/src/selection/types.ts +13 -320
  76. package/src/utils/aliasGenerator.ts +1 -56
@@ -1,19 +1,25 @@
1
- import { GraphQlClient } from '@contember/graphql-client'
2
1
  import {
3
2
  ContentClient,
4
- ContentQueryBuilder,
5
- ContentEntitySelection,
6
- SchemaNames,
7
- ContentQuery,
8
- type EntitySelectionManyArgs,
9
- type EntitySelectionOneArgs,
10
- type EntitySelectionColumnArgs,
11
- type ContentClientInput,
12
- } from '@contember/client-content'
13
- import { type Input } from '@contember/schema'
3
+ type ContentClientOptions,
4
+ ContentOperation,
5
+ type ContentQuery,
6
+ querySpecToGraphQl,
7
+ type QuerySpecContext,
8
+ buildGetArgs,
9
+ buildListArgs,
10
+ buildCreateArgs,
11
+ buildUpdateArgs,
12
+ buildDeleteArgs,
13
+ buildMutationSelection,
14
+ mutationFragments,
15
+ unwrapPaginateResult,
16
+ } from '@contember/bindx-client'
17
+ import type { GraphQlClient } from '@contember/graphql-client'
14
18
  import type { QuerySpec, QueryFieldSpec } from '../selection/buildQuery.js'
15
19
  import type { BackendAdapter, Query, QueryResult, QueryOptions, GetQuery, ListQuery, PersistResult, CreateResult, DeleteResult } from './types.js'
16
20
  import type { ContemberMutationResult } from '../errors/pathMapper.js'
21
+ import type { SchemaRegistry } from '../schema/SchemaRegistry.js'
22
+ import { GraphQlField } from '@contember/graphql-builder'
17
23
 
18
24
  /**
19
25
  * Options for ContemberAdapter
@@ -21,19 +27,19 @@ import type { ContemberMutationResult } from '../errors/pathMapper.js'
21
27
  export interface ContemberAdapterOptions {
22
28
  /** GraphQL client instance */
23
29
  client: GraphQlClient
24
- /** Contember schema names for query building */
25
- schema: SchemaNames
30
+ /** Schema registry for relation target resolution */
31
+ schemaRegistry: SchemaRegistry
26
32
  }
27
33
 
28
34
  /**
29
35
  * Backend adapter for Contember Content API.
30
- * Uses @contember/client-content for type-safe GraphQL operations.
36
+ * Uses @contember/bindx-client for GraphQL operations.
31
37
  */
32
38
  export class ContemberAdapter implements BackendAdapter {
33
39
 
34
40
  constructor(
35
41
  private readonly contentClient: ContentClient,
36
- private readonly queryBuilder: ContentQueryBuilder,
42
+ private readonly schemaRegistry: SchemaRegistry,
37
43
  ) {
38
44
  }
39
45
 
@@ -59,34 +65,52 @@ export class ContemberAdapter implements BackendAdapter {
59
65
  signal: options?.signal,
60
66
  })
61
67
 
62
- // Map results back to QueryResult array
63
- // Content client transforms already unwrap paginateRelation → flat arrays
68
+ // Map results back to QueryResult array, unwrapping paginate format
64
69
  return queries.map((q, i) => {
65
70
  const key = `q${i}`
66
71
  const data = (results as Record<string, unknown>)[key]
67
72
 
68
73
  if (q.type === 'get') {
69
- return { type: 'get' as const, data: data as Record<string, unknown> | null }
74
+ const unwrapped = data ? unwrapPaginateFields(data as Record<string, unknown>, q.spec) : null
75
+ return { type: 'get' as const, data: unwrapped }
70
76
  } else {
71
- return { type: 'list' as const, data: (data ?? []) as readonly Record<string, unknown>[] }
77
+ const items = (data ?? []) as readonly Record<string, unknown>[]
78
+ return { type: 'list' as const, data: items.map(item => unwrapPaginateFields(item, q.spec)) }
72
79
  }
73
80
  })
74
81
  }
75
82
 
76
83
  private buildGetQuery(query: GetQuery): ContentQuery<unknown> {
77
- const selection = this.buildEntitySelection(query.spec)
78
- return this.queryBuilder.get(query.entityType, { by: query.by }, selection)
84
+ const context = this.createContext(query.entityType)
85
+ const selectionSet = querySpecToGraphQl(query.spec, context)
86
+ const args = buildGetArgs(query.entityType, { by: query.by })
87
+
88
+ return new ContentOperation(
89
+ 'query',
90
+ `get${query.entityType}`,
91
+ args,
92
+ selectionSet,
93
+ value => value,
94
+ )
79
95
  }
80
96
 
81
97
  private buildListQuery(query: ListQuery): ContentQuery<unknown> {
82
- const selection = this.buildEntitySelection(query.spec)
83
- const args: ContentClientInput.AnyListQueryInput = {
84
- filter: query.filter as Input.OptionalWhere | undefined,
85
- orderBy: query.orderBy as ContentClientInput.AnyOrderBy | undefined,
98
+ const context = this.createContext(query.entityType)
99
+ const selectionSet = querySpecToGraphQl(query.spec, context)
100
+ const args = buildListArgs(query.entityType, {
101
+ filter: query.filter,
102
+ orderBy: query.orderBy as unknown[],
86
103
  limit: query.limit,
87
104
  offset: query.offset,
88
- }
89
- return this.queryBuilder.list(query.entityType, args, selection)
105
+ })
106
+
107
+ return new ContentOperation(
108
+ 'query',
109
+ `list${query.entityType}`,
110
+ args,
111
+ selectionSet,
112
+ value => value,
113
+ )
90
114
  }
91
115
 
92
116
  async persist(
@@ -94,84 +118,117 @@ export class ContemberAdapter implements BackendAdapter {
94
118
  id: string,
95
119
  changes: Record<string, unknown>,
96
120
  ): Promise<PersistResult> {
97
- const mutation = this.queryBuilder.update(entityType, {
98
- by: { id },
99
- data: changes as Input.UpdateDataInput,
100
- })
121
+ const args = buildUpdateArgs(entityType, { by: { id }, data: changes })
122
+ const selectionSet = buildMutationSelection('update')
101
123
 
102
- const result = await this.contentClient.mutate(mutation)
124
+ const mutation = new ContentOperation(
125
+ 'mutation',
126
+ `update${entityType}`,
127
+ args,
128
+ selectionSet,
129
+ value => value as { ok: boolean; errorMessage: string | null; errors: unknown[]; validation: unknown },
130
+ )
103
131
 
104
- if (!result.ok) {
105
- return {
106
- ok: false,
107
- errorMessage: result.errorMessage ?? `Failed to update ${entityType}:${id}`,
108
- mutationResult: this.toMutationResult(result),
132
+ try {
133
+ const result = await this.contentClient.mutate(mutation)
134
+ return { ok: true }
135
+ } catch (e) {
136
+ if (e instanceof Error && 'result' in e) {
137
+ const mutResult = (e as { result: Record<string, unknown> }).result
138
+ return {
139
+ ok: false,
140
+ errorMessage: (mutResult['errorMessage'] as string | null) ?? `Failed to update ${entityType}:${id}`,
141
+ mutationResult: this.toMutationResult(mutResult),
142
+ }
109
143
  }
144
+ throw e
110
145
  }
111
-
112
- return { ok: true }
113
146
  }
114
147
 
115
148
  async create(
116
149
  entityType: string,
117
150
  data: Record<string, unknown>,
118
151
  ): Promise<CreateResult> {
119
- // Build selection to return created entity
120
- const selection = (s: ContentEntitySelection) => s.$('id')
152
+ const args = buildCreateArgs(entityType, { data })
153
+ // Select id on created node
154
+ const nodeSelection = [new GraphQlField(null, 'id')]
155
+ const selectionSet = buildMutationSelection('create', nodeSelection)
121
156
 
122
- const mutation = this.queryBuilder.create(entityType, { data: data as Input.CreateDataInput }, selection)
123
- const result = await this.contentClient.mutate(mutation)
157
+ const mutation = new ContentOperation(
158
+ 'mutation',
159
+ `create${entityType}`,
160
+ args,
161
+ selectionSet,
162
+ value => value as { ok: boolean; errorMessage: string | null; errors: unknown[]; validation: unknown; node: unknown },
163
+ )
124
164
 
125
- if (!result.ok) {
165
+ try {
166
+ const result = await this.contentClient.mutate(mutation)
167
+ const typedResult = result as { node: Record<string, unknown> | null }
126
168
  return {
127
- ok: false,
128
- errorMessage: result.errorMessage ?? `Failed to create ${entityType}`,
129
- mutationResult: this.toMutationResult(result),
169
+ ok: true,
170
+ data: typedResult.node as Record<string, unknown>,
130
171
  }
131
- }
132
-
133
- return {
134
- ok: true,
135
- data: result.node as Record<string, unknown>,
172
+ } catch (e) {
173
+ if (e instanceof Error && 'result' in e) {
174
+ const mutResult = (e as { result: Record<string, unknown> }).result
175
+ return {
176
+ ok: false,
177
+ errorMessage: (mutResult['errorMessage'] as string | null) ?? `Failed to create ${entityType}`,
178
+ mutationResult: this.toMutationResult(mutResult),
179
+ }
180
+ }
181
+ throw e
136
182
  }
137
183
  }
138
184
 
139
185
  async delete(entityType: string, id: string): Promise<DeleteResult> {
140
- const mutation = this.queryBuilder.delete(entityType, { by: { id } })
141
- const result = await this.contentClient.mutate(mutation)
186
+ const args = buildDeleteArgs(entityType, { by: { id } })
187
+ const selectionSet = buildMutationSelection('delete')
142
188
 
143
- if (!result.ok) {
144
- return {
145
- ok: false,
146
- errorMessage: result.errorMessage ?? `Failed to delete ${entityType}:${id}`,
147
- mutationResult: this.toMutationResult(result),
189
+ const mutation = new ContentOperation(
190
+ 'mutation',
191
+ `delete${entityType}`,
192
+ args,
193
+ selectionSet,
194
+ value => value as { ok: boolean; errorMessage: string | null; errors: unknown[] },
195
+ )
196
+
197
+ try {
198
+ const result = await this.contentClient.mutate(mutation)
199
+ return { ok: true }
200
+ } catch (e) {
201
+ if (e instanceof Error && 'result' in e) {
202
+ const mutResult = (e as { result: Record<string, unknown> }).result
203
+ return {
204
+ ok: false,
205
+ errorMessage: (mutResult['errorMessage'] as string | null) ?? `Failed to delete ${entityType}:${id}`,
206
+ mutationResult: this.toMutationResult(mutResult),
207
+ }
148
208
  }
209
+ throw e
149
210
  }
150
-
151
- return { ok: true }
152
211
  }
153
212
 
154
213
  /**
155
- * Converts Contember mutation result to our ContemberMutationResult type.
214
+ * Converts raw mutation result to ContemberMutationResult type.
156
215
  */
157
- private toMutationResult(result: {
158
- ok: boolean
159
- errorMessage: string | null
160
- errors: Array<{ paths: Array<Array<{ field: string } | { index: number; alias: string | null }>>; message: string; type: string }>
161
- validation: { valid: boolean; errors: Array<{ path: Array<{ field: string } | { index: number; alias: string | null }>; message: { text: string } }> }
162
- }): ContemberMutationResult {
216
+ private toMutationResult(result: Record<string, unknown>): ContemberMutationResult {
217
+ const errors = (result['errors'] as Array<{ paths: unknown; message: string; type: string }>) ?? []
218
+ const validation = result['validation'] as { valid: boolean; errors: Array<{ path: unknown; message: { text: string } }> } | undefined
219
+
163
220
  return {
164
- ok: result.ok,
165
- errorMessage: result.errorMessage,
166
- errors: result.errors.map(e => ({
167
- paths: e.paths,
221
+ ok: result['ok'] as boolean,
222
+ errorMessage: result['errorMessage'] as string | null,
223
+ errors: errors.map(e => ({
224
+ paths: e.paths as Array<Array<{ field: string } | { index: number; alias: string | null }>>,
168
225
  message: e.message,
169
226
  type: e.type,
170
227
  })),
171
228
  validation: {
172
- valid: result.validation.valid,
173
- errors: result.validation.errors.map(e => ({
174
- path: e.path,
229
+ valid: validation?.valid ?? true,
230
+ errors: (validation?.errors ?? []).map(e => ({
231
+ path: e.path as Array<{ field: string } | { index: number; alias: string | null }>,
175
232
  message: { text: e.message.text },
176
233
  })),
177
234
  },
@@ -179,61 +236,74 @@ export class ContemberAdapter implements BackendAdapter {
179
236
  }
180
237
 
181
238
  /**
182
- * Builds ContentEntitySelection from QuerySpec
239
+ * Creates a QuerySpecContext for an entity, wiring in SchemaRegistry for relation target resolution.
183
240
  */
184
- private buildEntitySelection(
185
- query: QuerySpec,
186
- ): (selection: ContentEntitySelection) => ContentEntitySelection {
187
- return (selection: ContentEntitySelection) => {
188
- return this.applyFieldsToSelection(selection, query.fields)
241
+ private createContext(entityName: string): QuerySpecContext {
242
+ return {
243
+ entityName,
244
+ resolveRelationTarget: (entity, field) => this.schemaRegistry.getRelationTarget(entity, field),
189
245
  }
190
246
  }
247
+ }
191
248
 
192
- /**
193
- * Recursively applies QueryFieldSpec[] to ContentEntitySelection
194
- */
195
- private applyFieldsToSelection(
196
- selection: ContentEntitySelection,
197
- fields: QueryFieldSpec[],
198
- ): ContentEntitySelection {
199
- let result = selection
200
-
201
- for (const field of fields) {
202
- const fieldName = field.sourcePath[0]
203
- if (!fieldName) continue
204
-
205
- if (field.nested) {
206
- // Relation field (has-one or has-many)
207
- const nestedCallback = (s: ContentEntitySelection) =>
208
- this.applyFieldsToSelection(s, field.nested!.fields)
209
-
210
- if (field.isArray) {
211
- // has-many relation — always uses paginateRelation
212
- const manyArgs: EntitySelectionManyArgs = {
213
- ...(field.name !== fieldName ? { as: field.name } : undefined),
214
- ...(field.filter !== undefined ? { filter: field.filter as Input.OptionalWhere } : undefined),
215
- ...(field.orderBy !== undefined ? { orderBy: field.orderBy as ContentClientInput.AnyOrderBy } : undefined),
216
- ...(field.limit !== undefined ? { limit: field.limit } : undefined),
217
- ...(field.offset !== undefined ? { offset: field.offset } : undefined),
218
- ...(field.totalCount ? { totalCount: true } : undefined),
219
- }
220
-
221
- result = result.$(fieldName, manyArgs, nestedCallback)
222
- } else {
223
- // has-one relation
224
- const oneArgs: EntitySelectionOneArgs = {
225
- ...(field.name !== fieldName ? { as: field.name } : undefined),
226
- }
227
- result = result.$(fieldName, oneArgs, nestedCallback)
228
- }
249
+ /**
250
+ * Recursively unwraps paginate connection format in entity data.
251
+ *
252
+ * The GraphQL layer uses `paginateFieldName { edges { node { ... } } }` for has-many relations,
253
+ * but the store expects flat arrays keyed by the original field name.
254
+ * This function uses the QuerySpec to identify which fields need unwrapping.
255
+ */
256
+ function unwrapPaginateFields(data: Record<string, unknown>, spec: QuerySpec): Record<string, unknown> {
257
+ const result: Record<string, unknown> = {}
258
+
259
+ // Copy all fields from data first
260
+ const handledKeys = new Set<string>()
261
+
262
+ for (const field of spec.fields) {
263
+ const fieldName = field.sourcePath[0]
264
+ if (!fieldName) continue
265
+
266
+ if (field.isArray && field.nested) {
267
+ // Has-many field — look for paginate key or alias
268
+ const paginateKey = `paginate${fieldName.charAt(0).toUpperCase()}${fieldName.slice(1)}`
269
+ const alias = field.name !== fieldName ? field.name : null
270
+ const lookupKey = alias ?? paginateKey
271
+
272
+ const value = data[lookupKey]
273
+ handledKeys.add(lookupKey)
274
+
275
+ if (value && typeof value === 'object' && 'edges' in value) {
276
+ const connection = value as { edges: { node: unknown }[]; pageInfo?: { totalCount: number } }
277
+ const items = unwrapPaginateResult(connection, !!field.totalCount) as Record<string, unknown>[]
278
+ result[field.name] = items.map(item => unwrapPaginateFields(item, field.nested!))
229
279
  } else {
230
- // Scalar field
231
- const columnArgs: EntitySelectionColumnArgs | undefined =
232
- field.name !== fieldName ? { as: field.name } : undefined
233
- result = result.$(fieldName, columnArgs)
280
+ result[field.name] = value
234
281
  }
282
+ } else if (field.nested) {
283
+ // Has-one relation — recurse into nested
284
+ const lookupKey = field.name !== fieldName ? field.name : fieldName
285
+ const value = data[lookupKey]
286
+ handledKeys.add(lookupKey)
287
+
288
+ if (value && typeof value === 'object') {
289
+ result[field.name] = unwrapPaginateFields(value as Record<string, unknown>, field.nested)
290
+ } else {
291
+ result[field.name] = value
292
+ }
293
+ } else {
294
+ // Scalar field
295
+ const lookupKey = field.name !== fieldName ? field.name : fieldName
296
+ handledKeys.add(lookupKey)
297
+ result[field.name] = data[lookupKey]
235
298
  }
299
+ }
236
300
 
237
- return result
301
+ // Preserve any extra fields not in the spec (e.g. 'id' returned by API)
302
+ for (const key of Object.keys(data)) {
303
+ if (!handledKeys.has(key) && !(key in result)) {
304
+ result[key] = data[key]
305
+ }
238
306
  }
307
+
308
+ return result
239
309
  }
@@ -1,28 +1 @@
1
- /**
2
- * ComponentBrand provides unique identity for each component created with createComponent.
3
- * Each instance has a unique runtime Symbol for brand validation.
4
- * The class structure itself provides nominal typing in TypeScript.
5
- */
6
- export class ComponentBrand {
7
- /**
8
- * Private property to ensure nominal typing.
9
- * Each class instance is considered a unique type.
10
- */
11
- private readonly __nominal = true
12
-
13
- /**
14
- * Runtime symbol for brand validation.
15
- * Used to check that EntityRef includes required component brands.
16
- */
17
- readonly brandSymbol: symbol
18
-
19
- constructor(public readonly name: string) {
20
- this.brandSymbol = Symbol(`component_${name}_${Math.random().toString(36).slice(2)}`)
21
- }
22
- }
23
-
24
- /**
25
- * Type alias for any ComponentBrand.
26
- * Used as default/base type in generic constraints.
27
- */
28
- export type AnyBrand = ComponentBrand
1
+ export { ComponentBrand, type AnyBrand } from '@contember/bindx-client'
@@ -6,12 +6,5 @@
6
6
 
7
7
  // Re-export unified MutationCollector from persistence
8
8
  export { MutationCollector, type EntityMutationResult } from '../persistence/MutationCollector.js'
9
- export { ContemberSchemaMutationAdapter } from '../persistence/ContemberSchemaMutationAdapter.js'
9
+ export { ContemberSchemaMutationAdapter, type SchemaNames } from '../persistence/ContemberSchemaMutationAdapter.js'
10
10
  export type { MutationSchemaProvider } from '../persistence/MutationSchemaProvider.js'
11
-
12
- // Re-export commonly used types from @contember/client-content
13
- export type {
14
- SchemaNames,
15
- SchemaEntityNames,
16
- ContentClientInput,
17
- } from '@contember/client-content'