@stonecrop/graphql-client 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,280 +0,0 @@
1
- /**
2
- * Query builder for constructing PostGraphile native queries with nested link selections.
3
- *
4
- * Instead of using the `stonecropRecord`/`stonecropRecords` resolvers which return JSON blobs,
5
- * this module builds native PostGraphile queries that leverage the ORM's relationship resolution
6
- * for efficient single-query fetches with JOINs.
7
- *
8
- * @example
9
- * ```ts
10
- * // Instead of:
11
- * // stonecropRecord(doctype: "SalesOrder", id: "...") { data }
12
- * // Which returns { customerId: "uuid" }
13
- *
14
- * // We generate:
15
- * // salesOrderById(id: "...") { id, customerId, partyByCustomerId { id, partyName } }
16
- * // Which returns { customerId: "uuid", partyByCustomerId: { id: "uuid", partyName: "Acme" } }
17
- * ```
18
- *
19
- * @public
20
- */
21
-
22
- import type { DoctypeMeta, ValueField } from '@stonecrop/schema'
23
- import { flattenFields, componentLinkExpansion } from '@stonecrop/schema'
24
-
25
- /**
26
- * Options for building queries
27
- * @public
28
- */
29
- export interface QueryBuilderOptions {
30
- /**
31
- * All available doctype metadata. Used to resolve target doctypes for link fields.
32
- */
33
- allMeta: DoctypeMeta[]
34
-
35
- /**
36
- * Maximum depth for nested link resolution. Defaults to 1 (immediate links only).
37
- * Set to 0 to disable link expansion.
38
- */
39
- maxDepth?: number
40
- }
41
-
42
- /**
43
- * Result of building a native query
44
- * @public
45
- */
46
- export interface BuiltQuery {
47
- /**
48
- * The GraphQL query string
49
- */
50
- query: string
51
-
52
- /**
53
- * Field names that are link fields with nested selections.
54
- * The consumer can use this to know which fields will have relationship data.
55
- */
56
- linkFields: string[]
57
- }
58
-
59
- /**
60
- * Convert a PascalCase doctype name to the camelCase query name PostGraphile uses.
61
- *
62
- * @example
63
- * doctypeToQueryName('SalesOrder') // 'salesOrder'
64
- * doctypeToQueryName('Party') // 'party'
65
- * @public
66
- */
67
- export function doctypeToQueryName(doctypeName: string): string {
68
- return doctypeName[0].toLowerCase() + doctypeName.slice(1)
69
- }
70
-
71
- /**
72
- * Convert a PascalCase doctype name to the PostGraphile single-record query name.
73
- *
74
- * @example
75
- * doctypeToSingleQuery('SalesOrder') // 'salesOrderById'
76
- * @public
77
- */
78
- export function doctypeToSingleQuery(doctypeName: string): string {
79
- return doctypeToQueryName(doctypeName) + 'ById'
80
- }
81
-
82
- /**
83
- * Convert a PascalCase doctype name to the PostGraphile list query name.
84
- *
85
- * @example
86
- * doctypeToListQuery('SalesOrder') // 'allSalesOrders'
87
- * doctypeToListQuery('Party') // 'allParties'
88
- * @public
89
- */
90
- export function doctypeToListQuery(doctypeName: string): string {
91
- const name = doctypeName
92
- if (name.endsWith('y')) {
93
- return 'all' + name.slice(0, -1) + 'ies'
94
- }
95
- return 'all' + name + 's'
96
- }
97
-
98
- /**
99
- * Build the PostGraphile relationship field name for a foreign key.
100
- *
101
- * PostGraphile names relationships as `targetTypeByFkField` in camelCase.
102
- *
103
- * @example
104
- * buildRelationshipName('Party', 'customerId') // 'partyByCustomerId'
105
- * buildRelationshipName('Company', 'companyId') // 'companyByCompanyId'
106
- * @public
107
- */
108
- export function buildRelationshipName(targetDoctypeName: string, fkFieldname: string): string {
109
- const prefix = doctypeToQueryName(targetDoctypeName)
110
- const suffix = fkFieldname[0].toUpperCase() + fkFieldname.slice(1)
111
- return prefix + 'By' + suffix
112
- }
113
-
114
- /**
115
- * Resolve a doctype slug to its metadata.
116
- */
117
- function resolveDoctype(slug: string, allMeta: DoctypeMeta[]): DoctypeMeta | undefined {
118
- return allMeta.find(m => m.slug === slug || m.name === slug)
119
- }
120
-
121
- function valueFieldNamed(fields: DoctypeMeta['fields'], fieldname: string): ValueField | undefined {
122
- for (const field of flattenFields(fields)) {
123
- if (field.kind === 'field' && field.fieldname === fieldname) {
124
- return field
125
- }
126
- }
127
- return undefined
128
- }
129
-
130
- /**
131
- * Build the field selection for a doctype, including nested selections for link fields.
132
- */
133
- function buildFieldSelection(
134
- meta: DoctypeMeta,
135
- allMeta: DoctypeMeta[],
136
- depth: number,
137
- maxDepth: number,
138
- linkFieldsOut: string[]
139
- ): string {
140
- const flatFields = flattenFields(meta.fields)
141
- const selections: string[] = []
142
-
143
- for (const field of flatFields) {
144
- if (field.kind !== 'field') continue
145
-
146
- const isLinkField = field.doctype && componentLinkExpansion(field.component) === 'inline'
147
-
148
- if (isLinkField && depth < maxDepth) {
149
- const targetMeta = resolveDoctype(field.doctype!, allMeta)
150
- if (targetMeta) {
151
- const relationshipName = buildRelationshipName(targetMeta.name, field.fieldname)
152
- const displayField = targetMeta.displayField
153
-
154
- if (displayField) {
155
- selections.push(field.fieldname)
156
- selections.push(`${relationshipName} { id ${displayField} }`)
157
- linkFieldsOut.push(field.fieldname)
158
- } else {
159
- selections.push(field.fieldname)
160
- }
161
- } else {
162
- selections.push(field.fieldname)
163
- }
164
- } else {
165
- selections.push(field.fieldname)
166
- }
167
- }
168
-
169
- return selections.join(' ')
170
- }
171
-
172
- /**
173
- * Build a native PostGraphile query for fetching a single record by ID.
174
- * @public
175
- */
176
- export function buildSingleRecordQuery(meta: DoctypeMeta, options: QueryBuilderOptions): BuiltQuery {
177
- const maxDepth = options.maxDepth ?? 1
178
- const linkFields: string[] = []
179
-
180
- const fieldSelection = buildFieldSelection(meta, options.allMeta, 0, maxDepth, linkFields)
181
- const queryName = doctypeToSingleQuery(meta.name)
182
-
183
- const query = `query($id: UUID!) { ${queryName}(id: $id) { ${fieldSelection} } }`
184
-
185
- return { query, linkFields }
186
- }
187
-
188
- /**
189
- * Build a native PostGraphile query for fetching multiple records.
190
- * @public
191
- */
192
- export function buildListRecordQuery(
193
- meta: DoctypeMeta,
194
- options: QueryBuilderOptions & {
195
- first?: number
196
- offset?: number
197
- orderBy?: string
198
- condition?: Record<string, unknown>
199
- }
200
- ): BuiltQuery {
201
- const maxDepth = options.maxDepth ?? 1
202
- const linkFields: string[] = []
203
-
204
- const fieldSelection = buildFieldSelection(meta, options.allMeta, 0, maxDepth, linkFields)
205
- const queryName = doctypeToListQuery(meta.name)
206
-
207
- const params: string[] = []
208
- const args: string[] = []
209
-
210
- if (options.first !== undefined) {
211
- params.push('$first: Int')
212
- args.push('first: $first')
213
- }
214
- if (options.offset !== undefined) {
215
- params.push('$offset: Int')
216
- args.push('offset: $offset')
217
- }
218
- if (options.orderBy) {
219
- params.push('$orderBy: [SalesOrdersOrderBy!]')
220
- args.push('orderBy: $orderBy')
221
- }
222
- if (options.condition) {
223
- params.push('$condition: SalesOrderCondition')
224
- args.push('condition: $condition')
225
- }
226
-
227
- const paramStr = params.length > 0 ? `(${params.join(', ')})` : ''
228
- const argStr = args.length > 0 ? `(${args.join(', ')})` : ''
229
-
230
- const query = `query${paramStr} { ${queryName}${argStr} { nodes { ${fieldSelection} } } }`
231
-
232
- return { query, linkFields }
233
- }
234
-
235
- /**
236
- * Transform a record fetched via native PostGraphile query to the flat format
237
- * expected by the Stonecrop client. Link fields become objects with `id` and `displayText`.
238
- * @public
239
- */
240
- export function transformNativeRecord(
241
- record: Record<string, unknown>,
242
- linkFields: string[],
243
- meta: DoctypeMeta,
244
- allMeta: DoctypeMeta[]
245
- ): Record<string, unknown> {
246
- const result: Record<string, unknown> = {}
247
-
248
- for (const [key, value] of Object.entries(record)) {
249
- if (key.includes('By')) {
250
- continue
251
- }
252
-
253
- if (linkFields.includes(key)) {
254
- const linkField = valueFieldNamed(meta.fields, key)
255
- if (linkField?.doctype) {
256
- const targetMeta = resolveDoctype(linkField.doctype, allMeta)
257
- if (targetMeta?.displayField) {
258
- const relationshipName = buildRelationshipName(targetMeta.name, key)
259
- const nestedRaw = record[relationshipName]
260
- const displayText =
261
- nestedRaw !== null && nestedRaw !== undefined && typeof nestedRaw === 'object'
262
- ? Reflect.get(nestedRaw, targetMeta.displayField)
263
- : undefined
264
-
265
- if (displayText !== undefined && displayText !== null && displayText !== '') {
266
- result[key] = {
267
- id: value,
268
- displayText,
269
- }
270
- continue
271
- }
272
- }
273
- }
274
- }
275
-
276
- result[key] = value
277
- }
278
-
279
- return result
280
- }