@stonecrop/graphql-client 0.20.0 → 0.22.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.
@@ -15,8 +15,8 @@ export interface StonecropClientOptions {
15
15
  /**
16
16
  * Client for interacting with Stonecrop GraphQL API.
17
17
  *
18
- * Acts as a transport layer — it passes requests to the middleware and returns
19
- * merged results. Does not construct queries itself.
18
+ * Acts as a transport layer for stonecropRecord/stonecropRecords/stonecropAction, and
19
+ * builds native PostGraphile queries for getNativeRecord/getNativeRecords.
20
20
  *
21
21
  * @public
22
22
  */
@@ -70,6 +70,37 @@ export declare class StonecropClient implements DataClient {
70
70
  * @param options - Query options (filters, orderBy, limit, offset)
71
71
  */
72
72
  getRecords(doctype: DoctypeRef, options?: GetRecordsOptions): Promise<GetRecordsResult>;
73
+ /**
74
+ * Get a single record by ID using PostGraphile's native query with relationship expansion.
75
+ *
76
+ * Unlike `getRecord()` which uses the `stonecropRecord` resolver returning a JSON blob,
77
+ * this method builds a native PostGraphile query that leverages the ORM's relationship
78
+ * resolution for efficient single-query fetches with JOINs.
79
+ *
80
+ * Link fields are returned as `{ id, displayText }` objects where `displayText` is
81
+ * resolved from the target doctype's `displayField`.
82
+ *
83
+ * @param doctype - Doctype reference (name and optional slug)
84
+ * @param recordId - Record ID to fetch
85
+ */
86
+ getNativeRecord(doctype: DoctypeRef, recordId: string): Promise<GetRecordResult>;
87
+ /**
88
+ * Get multiple records using PostGraphile's native query with relationship expansion.
89
+ *
90
+ * Unlike `getRecords()` which uses the `stonecropRecords` resolver returning JSON blobs,
91
+ * this method builds a native PostGraphile query that leverages the ORM's relationship
92
+ * resolution for efficient single-query fetches with JOINs.
93
+ *
94
+ * Link fields are returned as `{ id, displayText }` objects where `displayText` is
95
+ * resolved from the target doctype's `displayField`.
96
+ *
97
+ * @param doctype - Doctype reference (name and optional slug)
98
+ * @param options - Query options (limit, offset)
99
+ */
100
+ getNativeRecords(doctype: DoctypeRef, options?: {
101
+ limit?: number;
102
+ offset?: number;
103
+ }): Promise<GetRecordsResult>;
73
104
  /**
74
105
  * Execute a doctype action
75
106
  * @param doctype - Doctype reference (name and optional slug)
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,WAAW,EACX,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAG9C,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAA;AAEjD;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,UAAU;IACjD,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,SAAS,CAAsC;gBAE3C,OAAO,EAAE,sBAAsB;IAQ3C;;;;;;OAMG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAoBxF;;;;;OAKG;IACG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAI5F;;;OAGG;IACG,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAenE;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAU1C;;;;;;;;;OASG;IACG,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IA4B5G;;;;;;;;OAQG;IACG,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuC7F;;;;;OAKG;IACG,SAAS,CACd,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,OAAO,EAAE,GACd,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAYrE;;;;;OAKG;IACH,cAAc,IAAI,IAAI;CAGtB"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,UAAU,EACV,WAAW,EACX,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAU9C,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAA;AAEjD;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACtC,2BAA2B;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAED;;;;;;;GAOG;AACH,qBAAa,eAAgB,YAAW,UAAU;IACjD,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,SAAS,CAAsC;gBAE3C,OAAO,EAAE,sBAAsB;IAQ3C;;;;;;OAMG;IACG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAoBxF;;;;;OAKG;IACG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAI5F;;;OAGG;IACG,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAenE;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAU1C;;;;;;;;;OASG;IACG,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IA4B5G;;;;;;;;OAQG;IACG,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAuC7F;;;;;;;;;;;;OAYG;IACG,eAAe,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAsBtF;;;;;;;;;;;;OAYG;IACG,gBAAgB,CACrB,OAAO,EAAE,UAAU,EACnB,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3C,OAAO,CAAC,gBAAgB,CAAC;IA8B5B;;;;;OAKG;IACG,SAAS,CACd,OAAO,EAAE,UAAU,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,OAAO,EAAE,GACd,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAYrE;;;;;OAKG;IACH,cAAc,IAAI,IAAI;CAGtB"}
@@ -1,9 +1,10 @@
1
1
  import { GET_META_QUERY, GET_ALL_META_QUERY, RUN_ACTION_MUTATION } from './queries';
2
+ import { buildSingleRecordQuery, buildListRecordQuery, transformNativeRecord, doctypeToSingleQuery, doctypeToListQuery, } from './query-builder';
2
3
  /**
3
4
  * Client for interacting with Stonecrop GraphQL API.
4
5
  *
5
- * Acts as a transport layer — it passes requests to the middleware and returns
6
- * merged results. Does not construct queries itself.
6
+ * Acts as a transport layer for stonecropRecord/stonecropRecords/stonecropAction, and
7
+ * builds native PostGraphile queries for getNativeRecord/getNativeRecords.
7
8
  *
8
9
  * @public
9
10
  */
@@ -147,6 +148,73 @@ export class StonecropClient {
147
148
  // through keeps "not asked for" and "asked for, and it is zero" distinguishable.
148
149
  return count == null ? { data, hasMore } : { data, hasMore, count };
149
150
  }
151
+ /**
152
+ * Get a single record by ID using PostGraphile's native query with relationship expansion.
153
+ *
154
+ * Unlike `getRecord()` which uses the `stonecropRecord` resolver returning a JSON blob,
155
+ * this method builds a native PostGraphile query that leverages the ORM's relationship
156
+ * resolution for efficient single-query fetches with JOINs.
157
+ *
158
+ * Link fields are returned as `{ id, displayText }` objects where `displayText` is
159
+ * resolved from the target doctype's `displayField`.
160
+ *
161
+ * @param doctype - Doctype reference (name and optional slug)
162
+ * @param recordId - Record ID to fetch
163
+ */
164
+ async getNativeRecord(doctype, recordId) {
165
+ const allMeta = await this.getAllMeta();
166
+ const meta = allMeta.find(m => m.name === doctype.name || m.slug === doctype.name);
167
+ if (!meta) {
168
+ return { record: null };
169
+ }
170
+ const { query, linkFields } = buildSingleRecordQuery(meta, { allMeta });
171
+ const queryName = doctypeToSingleQuery(meta.name);
172
+ const result = await this.query(query, { id: recordId });
173
+ const rawRecord = result[queryName];
174
+ if (!rawRecord) {
175
+ return { record: null };
176
+ }
177
+ const record = transformNativeRecord(rawRecord, linkFields, meta, allMeta);
178
+ return { record };
179
+ }
180
+ /**
181
+ * Get multiple records using PostGraphile's native query with relationship expansion.
182
+ *
183
+ * Unlike `getRecords()` which uses the `stonecropRecords` resolver returning JSON blobs,
184
+ * this method builds a native PostGraphile query that leverages the ORM's relationship
185
+ * resolution for efficient single-query fetches with JOINs.
186
+ *
187
+ * Link fields are returned as `{ id, displayText }` objects where `displayText` is
188
+ * resolved from the target doctype's `displayField`.
189
+ *
190
+ * @param doctype - Doctype reference (name and optional slug)
191
+ * @param options - Query options (limit, offset)
192
+ */
193
+ async getNativeRecords(doctype, options) {
194
+ const allMeta = await this.getAllMeta();
195
+ const meta = allMeta.find(m => m.name === doctype.name || m.slug === doctype.name);
196
+ if (!meta) {
197
+ return { data: [], hasMore: false };
198
+ }
199
+ const { query, linkFields } = buildListRecordQuery(meta, {
200
+ allMeta,
201
+ first: options?.limit,
202
+ offset: options?.offset,
203
+ });
204
+ const queryName = doctypeToListQuery(meta.name);
205
+ const variables = {};
206
+ if (options?.limit !== undefined)
207
+ variables.first = options.limit;
208
+ if (options?.offset !== undefined)
209
+ variables.offset = options.offset;
210
+ const result = await this.query(query, variables);
211
+ const nodes = result[queryName]?.nodes ?? [];
212
+ const data = nodes.map(node => transformNativeRecord(node, linkFields, meta, allMeta));
213
+ return {
214
+ data,
215
+ hasMore: nodes.length === options?.limit,
216
+ };
217
+ }
150
218
  /**
151
219
  * Execute a doctype action
152
220
  * @param doctype - Doctype reference (name and optional slug)
@@ -1,4 +1,5 @@
1
1
  export type { DoctypeMeta } from '@stonecrop/schema';
2
2
  export { StonecropClient, type StonecropClientOptions, type DoctypeContext } from './client';
3
3
  export type { GetRecordResult } from './types';
4
+ export { buildSingleRecordQuery, buildListRecordQuery, transformNativeRecord, doctypeToSingleQuery, doctypeToListQuery, buildRelationshipName, doctypeToQueryName, type QueryBuilderOptions, type BuiltQuery, } from './query-builder';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAC5F,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAC5F,YAAY,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAC9C,OAAO,EACN,sBAAsB,EACtB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,UAAU,GACf,MAAM,iBAAiB,CAAA"}
package/dist/src/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { StonecropClient } from './client';
2
+ export { buildSingleRecordQuery, buildListRecordQuery, transformNativeRecord, doctypeToSingleQuery, doctypeToListQuery, buildRelationshipName, doctypeToQueryName, } from './query-builder';
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @public
11
11
  */
12
- export declare const GET_META_QUERY = "\n\tquery GetMeta($doctype: String!) {\n\t\tstonecropMeta(doctype: $doctype) {\n\t\t\tname\n\t\t\tslug\n\t\t\tfields {\n\t\t\t\tkind\n\t\t\t\tfieldname\n\t\t\t\tcomponent\n\t\t\t\tprimaryKey\n\t\t\t\tcomputed\n\t\t\t\tlanguage\n\t\t\t\tdoctype\n\t\t\t\tlabel\n\t\t\t\twidth\n\t\t\t\talign\n\t\t\t\tedit\n\t\t\t\tmask\n\t\t\t\tformat\n\t\t\t\tmode\n\t\t\t\toptions\n\t\t\t\trequired\n\t\t\t\treadOnly\n\t\t\t\thidden\n\t\t\t\tdefault\n\t\t\t\tvalidation\n\t\t\t\tcardinality\n\t\t\t\tsource\n\t\t\t}\n\t\t\tworkflow {\n\t\t\t\tstates\n\t\t\t\tactions {\n\t\t\t\t\tlabel\n\t\t\t\t\trequiredFields\n\t\t\t\t\tallowedStates\n\t\t\t\t\tnextState\n\t\t\t\t\tstateless\n\t\t\t\t\tselfTransition\n\t\t\t\t\tclientHandler\n\t\t\t\t}\n\t\t\t}\n\t\t\tinherits\n\t\t}\n\t}\n";
12
+ export declare const GET_META_QUERY = "\n\tquery GetMeta($doctype: String!) {\n\t\tstonecropMeta(doctype: $doctype) {\n\t\t\tname\n\t\t\tslug\n\t\t\tdisplayField\n\t\t\tfields {\n\t\t\t\tkind\n\t\t\t\tfieldname\n\t\t\t\tcomponent\n\t\t\t\tprimaryKey\n\t\t\t\tcomputed\n\t\t\t\tlanguage\n\t\t\t\tdoctype\n\t\t\t\tlabel\n\t\t\t\twidth\n\t\t\t\talign\n\t\t\t\tedit\n\t\t\t\tmask\n\t\t\t\tformat\n\t\t\t\tmode\n\t\t\t\toptions\n\t\t\t\trequired\n\t\t\t\treadOnly\n\t\t\t\thidden\n\t\t\t\tdefault\n\t\t\t\tvalidation\n\t\t\t\tcardinality\n\t\t\t\tsource\n\t\t\t}\n\t\t\tworkflow {\n\t\t\t\tstates\n\t\t\t\tactions {\n\t\t\t\t\tlabel\n\t\t\t\t\trequiredFields\n\t\t\t\t\tallowedStates\n\t\t\t\t\tnextState\n\t\t\t\t\tstateless\n\t\t\t\t\tselfTransition\n\t\t\t\t\tclientHandler\n\t\t\t\t}\n\t\t\t}\n\t\t\tinherits\n\t\t}\n\t}\n";
13
13
  /**
14
14
  * Mutation document for dispatching a workflow action (the server-owned transition).
15
15
  * @public
@@ -19,5 +19,5 @@ export declare const RUN_ACTION_MUTATION = "\n\tmutation RunAction($doctype: Str
19
19
  * Query document for fetching all doctype metadata.
20
20
  * @public
21
21
  */
22
- export declare const GET_ALL_META_QUERY = "\n\tquery GetAllMeta {\n\t\tstonecropAllMeta {\n\t\t\tname\n\t\t\tslug\n\t\t\tfields {\n\t\t\t\tkind\n\t\t\t\tfieldname\n\t\t\t\tcomponent\n\t\t\t\tprimaryKey\n\t\t\t\tcomputed\n\t\t\t\tlanguage\n\t\t\t\tdoctype\n\t\t\t\tlabel\n\t\t\t\twidth\n\t\t\t\talign\n\t\t\t\tedit\n\t\t\t\tmask\n\t\t\t\tformat\n\t\t\t\tmode\n\t\t\t\toptions\n\t\t\t\trequired\n\t\t\t\treadOnly\n\t\t\t\thidden\n\t\t\t\tdefault\n\t\t\t\tvalidation\n\t\t\t\tcardinality\n\t\t\t\tsource\n\t\t\t}\n\t\t\tworkflow {\n\t\t\t\tstates\n\t\t\t\tactions {\n\t\t\t\t\tlabel\n\t\t\t\t\trequiredFields\n\t\t\t\t\tallowedStates\n\t\t\t\t\tnextState\n\t\t\t\t\tstateless\n\t\t\t\t\tselfTransition\n\t\t\t\t\tclientHandler\n\t\t\t\t}\n\t\t\t}\n\t\t\tinherits\n\t\t}\n\t}\n";
22
+ export declare const GET_ALL_META_QUERY = "\n\tquery GetAllMeta {\n\t\tstonecropAllMeta {\n\t\t\tname\n\t\t\tslug\n\t\t\tdisplayField\n\t\t\tfields {\n\t\t\t\tkind\n\t\t\t\tfieldname\n\t\t\t\tcomponent\n\t\t\t\tprimaryKey\n\t\t\t\tcomputed\n\t\t\t\tlanguage\n\t\t\t\tdoctype\n\t\t\t\tlabel\n\t\t\t\twidth\n\t\t\t\talign\n\t\t\t\tedit\n\t\t\t\tmask\n\t\t\t\tformat\n\t\t\t\tmode\n\t\t\t\toptions\n\t\t\t\trequired\n\t\t\t\treadOnly\n\t\t\t\thidden\n\t\t\t\tdefault\n\t\t\t\tvalidation\n\t\t\t\tcardinality\n\t\t\t\tsource\n\t\t\t}\n\t\t\tworkflow {\n\t\t\t\tstates\n\t\t\t\tactions {\n\t\t\t\t\tlabel\n\t\t\t\t\trequiredFields\n\t\t\t\t\tallowedStates\n\t\t\t\t\tnextState\n\t\t\t\t\tstateless\n\t\t\t\t\tselfTransition\n\t\t\t\t\tclientHandler\n\t\t\t\t}\n\t\t\t}\n\t\t\tinherits\n\t\t}\n\t}\n";
23
23
  //# sourceMappingURL=queries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,+vBA4C1B,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,6MAQ/B,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,+tBA4C9B,CAAA"}
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../src/queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,mxBA6C1B,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,6MAQ/B,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,mvBA6C9B,CAAA"}
@@ -14,6 +14,7 @@ export const GET_META_QUERY = `
14
14
  stonecropMeta(doctype: $doctype) {
15
15
  name
16
16
  slug
17
+ displayField
17
18
  fields {
18
19
  kind
19
20
  fieldname
@@ -76,6 +77,7 @@ export const GET_ALL_META_QUERY = `
76
77
  stonecropAllMeta {
77
78
  name
78
79
  slug
80
+ displayField
79
81
  fields {
80
82
  kind
81
83
  fieldname
@@ -0,0 +1,110 @@
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
+ import type { DoctypeMeta } from '@stonecrop/schema';
22
+ /**
23
+ * Options for building queries
24
+ * @public
25
+ */
26
+ export interface QueryBuilderOptions {
27
+ /**
28
+ * All available doctype metadata. Used to resolve target doctypes for link fields.
29
+ */
30
+ allMeta: DoctypeMeta[];
31
+ /**
32
+ * Maximum depth for nested link resolution. Defaults to 1 (immediate links only).
33
+ * Set to 0 to disable link expansion.
34
+ */
35
+ maxDepth?: number;
36
+ }
37
+ /**
38
+ * Result of building a native query
39
+ * @public
40
+ */
41
+ export interface BuiltQuery {
42
+ /**
43
+ * The GraphQL query string
44
+ */
45
+ query: string;
46
+ /**
47
+ * Field names that are link fields with nested selections.
48
+ * The consumer can use this to know which fields will have relationship data.
49
+ */
50
+ linkFields: string[];
51
+ }
52
+ /**
53
+ * Convert a PascalCase doctype name to the camelCase query name PostGraphile uses.
54
+ *
55
+ * @example
56
+ * doctypeToQueryName('SalesOrder') // 'salesOrder'
57
+ * doctypeToQueryName('Party') // 'party'
58
+ * @public
59
+ */
60
+ export declare function doctypeToQueryName(doctypeName: string): string;
61
+ /**
62
+ * Convert a PascalCase doctype name to the PostGraphile single-record query name.
63
+ *
64
+ * @example
65
+ * doctypeToSingleQuery('SalesOrder') // 'salesOrderById'
66
+ * @public
67
+ */
68
+ export declare function doctypeToSingleQuery(doctypeName: string): string;
69
+ /**
70
+ * Convert a PascalCase doctype name to the PostGraphile list query name.
71
+ *
72
+ * @example
73
+ * doctypeToListQuery('SalesOrder') // 'allSalesOrders'
74
+ * doctypeToListQuery('Party') // 'allParties'
75
+ * @public
76
+ */
77
+ export declare function doctypeToListQuery(doctypeName: string): string;
78
+ /**
79
+ * Build the PostGraphile relationship field name for a foreign key.
80
+ *
81
+ * PostGraphile names relationships as `targetTypeByFkField` in camelCase.
82
+ *
83
+ * @example
84
+ * buildRelationshipName('Party', 'customerId') // 'partyByCustomerId'
85
+ * buildRelationshipName('Company', 'companyId') // 'companyByCompanyId'
86
+ * @public
87
+ */
88
+ export declare function buildRelationshipName(targetDoctypeName: string, fkFieldname: string): string;
89
+ /**
90
+ * Build a native PostGraphile query for fetching a single record by ID.
91
+ * @public
92
+ */
93
+ export declare function buildSingleRecordQuery(meta: DoctypeMeta, options: QueryBuilderOptions): BuiltQuery;
94
+ /**
95
+ * Build a native PostGraphile query for fetching multiple records.
96
+ * @public
97
+ */
98
+ export declare function buildListRecordQuery(meta: DoctypeMeta, options: QueryBuilderOptions & {
99
+ first?: number;
100
+ offset?: number;
101
+ orderBy?: string;
102
+ condition?: Record<string, unknown>;
103
+ }): BuiltQuery;
104
+ /**
105
+ * Transform a record fetched via native PostGraphile query to the flat format
106
+ * expected by the Stonecrop client. Link fields become objects with `id` and `displayText`.
107
+ * @public
108
+ */
109
+ export declare function transformNativeRecord(record: Record<string, unknown>, linkFields: string[], meta: DoctypeMeta, allMeta: DoctypeMeta[]): Record<string, unknown>;
110
+ //# sourceMappingURL=query-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-builder.d.ts","sourceRoot":"","sources":["../../src/query-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,mBAAmB,CAAA;AAGhE;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IACnC;;OAEG;IACH,OAAO,EAAE,WAAW,EAAE,CAAA;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IAC1B;;OAEG;IACH,KAAK,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,UAAU,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,iBAAiB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAI5F;AA4DD;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB,GAAG,UAAU,CAUlG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,mBAAmB,GAAG;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,GACC,UAAU,CAiCZ;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,UAAU,EAAE,MAAM,EAAE,EACpB,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,WAAW,EAAE,GACpB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmCzB"}
@@ -0,0 +1,199 @@
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
+ import { flattenFields, componentLinkExpansion } from '@stonecrop/schema';
22
+ /**
23
+ * Convert a PascalCase doctype name to the camelCase query name PostGraphile uses.
24
+ *
25
+ * @example
26
+ * doctypeToQueryName('SalesOrder') // 'salesOrder'
27
+ * doctypeToQueryName('Party') // 'party'
28
+ * @public
29
+ */
30
+ export function doctypeToQueryName(doctypeName) {
31
+ return doctypeName[0].toLowerCase() + doctypeName.slice(1);
32
+ }
33
+ /**
34
+ * Convert a PascalCase doctype name to the PostGraphile single-record query name.
35
+ *
36
+ * @example
37
+ * doctypeToSingleQuery('SalesOrder') // 'salesOrderById'
38
+ * @public
39
+ */
40
+ export function doctypeToSingleQuery(doctypeName) {
41
+ return doctypeToQueryName(doctypeName) + 'ById';
42
+ }
43
+ /**
44
+ * Convert a PascalCase doctype name to the PostGraphile list query name.
45
+ *
46
+ * @example
47
+ * doctypeToListQuery('SalesOrder') // 'allSalesOrders'
48
+ * doctypeToListQuery('Party') // 'allParties'
49
+ * @public
50
+ */
51
+ export function doctypeToListQuery(doctypeName) {
52
+ const name = doctypeName;
53
+ if (name.endsWith('y')) {
54
+ return 'all' + name.slice(0, -1) + 'ies';
55
+ }
56
+ return 'all' + name + 's';
57
+ }
58
+ /**
59
+ * Build the PostGraphile relationship field name for a foreign key.
60
+ *
61
+ * PostGraphile names relationships as `targetTypeByFkField` in camelCase.
62
+ *
63
+ * @example
64
+ * buildRelationshipName('Party', 'customerId') // 'partyByCustomerId'
65
+ * buildRelationshipName('Company', 'companyId') // 'companyByCompanyId'
66
+ * @public
67
+ */
68
+ export function buildRelationshipName(targetDoctypeName, fkFieldname) {
69
+ const prefix = doctypeToQueryName(targetDoctypeName);
70
+ const suffix = fkFieldname[0].toUpperCase() + fkFieldname.slice(1);
71
+ return prefix + 'By' + suffix;
72
+ }
73
+ /**
74
+ * Resolve a doctype slug to its metadata.
75
+ */
76
+ function resolveDoctype(slug, allMeta) {
77
+ return allMeta.find(m => m.slug === slug || m.name === slug);
78
+ }
79
+ function valueFieldNamed(fields, fieldname) {
80
+ for (const field of flattenFields(fields)) {
81
+ if (field.kind === 'field' && field.fieldname === fieldname) {
82
+ return field;
83
+ }
84
+ }
85
+ return undefined;
86
+ }
87
+ /**
88
+ * Build the field selection for a doctype, including nested selections for link fields.
89
+ */
90
+ function buildFieldSelection(meta, allMeta, depth, maxDepth, linkFieldsOut) {
91
+ const flatFields = flattenFields(meta.fields);
92
+ const selections = [];
93
+ for (const field of flatFields) {
94
+ if (field.kind !== 'field')
95
+ continue;
96
+ const isLinkField = field.doctype && componentLinkExpansion(field.component) === 'inline';
97
+ if (isLinkField && depth < maxDepth) {
98
+ const targetMeta = resolveDoctype(field.doctype, allMeta);
99
+ if (targetMeta) {
100
+ const relationshipName = buildRelationshipName(targetMeta.name, field.fieldname);
101
+ const displayField = targetMeta.displayField;
102
+ if (displayField) {
103
+ selections.push(field.fieldname);
104
+ selections.push(`${relationshipName} { id ${displayField} }`);
105
+ linkFieldsOut.push(field.fieldname);
106
+ }
107
+ else {
108
+ selections.push(field.fieldname);
109
+ }
110
+ }
111
+ else {
112
+ selections.push(field.fieldname);
113
+ }
114
+ }
115
+ else {
116
+ selections.push(field.fieldname);
117
+ }
118
+ }
119
+ return selections.join(' ');
120
+ }
121
+ /**
122
+ * Build a native PostGraphile query for fetching a single record by ID.
123
+ * @public
124
+ */
125
+ export function buildSingleRecordQuery(meta, options) {
126
+ const maxDepth = options.maxDepth ?? 1;
127
+ const linkFields = [];
128
+ const fieldSelection = buildFieldSelection(meta, options.allMeta, 0, maxDepth, linkFields);
129
+ const queryName = doctypeToSingleQuery(meta.name);
130
+ const query = `query($id: UUID!) { ${queryName}(id: $id) { ${fieldSelection} } }`;
131
+ return { query, linkFields };
132
+ }
133
+ /**
134
+ * Build a native PostGraphile query for fetching multiple records.
135
+ * @public
136
+ */
137
+ export function buildListRecordQuery(meta, options) {
138
+ const maxDepth = options.maxDepth ?? 1;
139
+ const linkFields = [];
140
+ const fieldSelection = buildFieldSelection(meta, options.allMeta, 0, maxDepth, linkFields);
141
+ const queryName = doctypeToListQuery(meta.name);
142
+ const params = [];
143
+ const args = [];
144
+ if (options.first !== undefined) {
145
+ params.push('$first: Int');
146
+ args.push('first: $first');
147
+ }
148
+ if (options.offset !== undefined) {
149
+ params.push('$offset: Int');
150
+ args.push('offset: $offset');
151
+ }
152
+ if (options.orderBy) {
153
+ params.push('$orderBy: [SalesOrdersOrderBy!]');
154
+ args.push('orderBy: $orderBy');
155
+ }
156
+ if (options.condition) {
157
+ params.push('$condition: SalesOrderCondition');
158
+ args.push('condition: $condition');
159
+ }
160
+ const paramStr = params.length > 0 ? `(${params.join(', ')})` : '';
161
+ const argStr = args.length > 0 ? `(${args.join(', ')})` : '';
162
+ const query = `query${paramStr} { ${queryName}${argStr} { nodes { ${fieldSelection} } } }`;
163
+ return { query, linkFields };
164
+ }
165
+ /**
166
+ * Transform a record fetched via native PostGraphile query to the flat format
167
+ * expected by the Stonecrop client. Link fields become objects with `id` and `displayText`.
168
+ * @public
169
+ */
170
+ export function transformNativeRecord(record, linkFields, meta, allMeta) {
171
+ const result = {};
172
+ for (const [key, value] of Object.entries(record)) {
173
+ if (key.includes('By')) {
174
+ continue;
175
+ }
176
+ if (linkFields.includes(key)) {
177
+ const linkField = valueFieldNamed(meta.fields, key);
178
+ if (linkField?.doctype) {
179
+ const targetMeta = resolveDoctype(linkField.doctype, allMeta);
180
+ if (targetMeta?.displayField) {
181
+ const relationshipName = buildRelationshipName(targetMeta.name, key);
182
+ const nestedRaw = record[relationshipName];
183
+ const displayText = nestedRaw !== null && nestedRaw !== undefined && typeof nestedRaw === 'object'
184
+ ? Reflect.get(nestedRaw, targetMeta.displayField)
185
+ : undefined;
186
+ if (displayText !== undefined && displayText !== null && displayText !== '') {
187
+ result[key] = {
188
+ id: value,
189
+ displayText,
190
+ };
191
+ continue;
192
+ }
193
+ }
194
+ }
195
+ }
196
+ result[key] = value;
197
+ }
198
+ return result;
199
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/graphql-client",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "GraphQL client integration for Stonecrop",
5
5
  "bugs": {
6
6
  "url": "https://github.com/agritheory/stonecrop/issues"
@@ -30,7 +30,7 @@
30
30
  "src/*"
31
31
  ],
32
32
  "dependencies": {
33
- "@stonecrop/schema": "0.20.0"
33
+ "@stonecrop/schema": "0.22.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@microsoft/api-documenter": "^7.30.5",
@@ -48,8 +48,8 @@
48
48
  "vitest": "^4.1.5",
49
49
  "vue": "^3.5.33",
50
50
  "vue-router": "^5.0.6",
51
- "stonecrop-rig": "0.7.0",
52
- "@stonecrop/graphql-middleware": "0.20.0"
51
+ "@stonecrop/graphql-middleware": "0.22.0",
52
+ "stonecrop-rig": "0.7.0"
53
53
  },
54
54
  "engines": {
55
55
  "node": ">=24.0.0"
package/src/client.ts CHANGED
@@ -9,6 +9,13 @@ import type {
9
9
  } from '@stonecrop/schema'
10
10
  import type { GetRecordResult } from './types'
11
11
  import { GET_META_QUERY, GET_ALL_META_QUERY, RUN_ACTION_MUTATION } from './queries'
12
+ import {
13
+ buildSingleRecordQuery,
14
+ buildListRecordQuery,
15
+ transformNativeRecord,
16
+ doctypeToSingleQuery,
17
+ doctypeToListQuery,
18
+ } from './query-builder'
12
19
 
13
20
  export type { DoctypeContext, DoctypeRef }
14
21
  export type { GetRecordResult, GetRecordsResult }
@@ -27,8 +34,8 @@ export interface StonecropClientOptions {
27
34
  /**
28
35
  * Client for interacting with Stonecrop GraphQL API.
29
36
  *
30
- * Acts as a transport layer — it passes requests to the middleware and returns
31
- * merged results. Does not construct queries itself.
37
+ * Acts as a transport layer for stonecropRecord/stonecropRecords/stonecropAction, and
38
+ * builds native PostGraphile queries for getNativeRecord/getNativeRecords.
32
39
  *
33
40
  * @public
34
41
  */
@@ -200,6 +207,87 @@ export class StonecropClient implements DataClient {
200
207
  return count == null ? { data, hasMore } : { data, hasMore, count }
201
208
  }
202
209
 
210
+ /**
211
+ * Get a single record by ID using PostGraphile's native query with relationship expansion.
212
+ *
213
+ * Unlike `getRecord()` which uses the `stonecropRecord` resolver returning a JSON blob,
214
+ * this method builds a native PostGraphile query that leverages the ORM's relationship
215
+ * resolution for efficient single-query fetches with JOINs.
216
+ *
217
+ * Link fields are returned as `{ id, displayText }` objects where `displayText` is
218
+ * resolved from the target doctype's `displayField`.
219
+ *
220
+ * @param doctype - Doctype reference (name and optional slug)
221
+ * @param recordId - Record ID to fetch
222
+ */
223
+ async getNativeRecord(doctype: DoctypeRef, recordId: string): Promise<GetRecordResult> {
224
+ const allMeta = await this.getAllMeta()
225
+ const meta = allMeta.find(m => m.name === doctype.name || m.slug === doctype.name)
226
+
227
+ if (!meta) {
228
+ return { record: null }
229
+ }
230
+
231
+ const { query, linkFields } = buildSingleRecordQuery(meta, { allMeta })
232
+ const queryName = doctypeToSingleQuery(meta.name)
233
+
234
+ const result = await this.query<Record<string, Record<string, unknown> | null>>(query, { id: recordId })
235
+
236
+ const rawRecord = result[queryName]
237
+ if (!rawRecord) {
238
+ return { record: null }
239
+ }
240
+
241
+ const record = transformNativeRecord(rawRecord, linkFields, meta, allMeta)
242
+ return { record }
243
+ }
244
+
245
+ /**
246
+ * Get multiple records using PostGraphile's native query with relationship expansion.
247
+ *
248
+ * Unlike `getRecords()` which uses the `stonecropRecords` resolver returning JSON blobs,
249
+ * this method builds a native PostGraphile query that leverages the ORM's relationship
250
+ * resolution for efficient single-query fetches with JOINs.
251
+ *
252
+ * Link fields are returned as `{ id, displayText }` objects where `displayText` is
253
+ * resolved from the target doctype's `displayField`.
254
+ *
255
+ * @param doctype - Doctype reference (name and optional slug)
256
+ * @param options - Query options (limit, offset)
257
+ */
258
+ async getNativeRecords(
259
+ doctype: DoctypeRef,
260
+ options?: { limit?: number; offset?: number }
261
+ ): Promise<GetRecordsResult> {
262
+ const allMeta = await this.getAllMeta()
263
+ const meta = allMeta.find(m => m.name === doctype.name || m.slug === doctype.name)
264
+
265
+ if (!meta) {
266
+ return { data: [], hasMore: false }
267
+ }
268
+
269
+ const { query, linkFields } = buildListRecordQuery(meta, {
270
+ allMeta,
271
+ first: options?.limit,
272
+ offset: options?.offset,
273
+ })
274
+ const queryName = doctypeToListQuery(meta.name)
275
+
276
+ const variables: Record<string, unknown> = {}
277
+ if (options?.limit !== undefined) variables.first = options.limit
278
+ if (options?.offset !== undefined) variables.offset = options.offset
279
+
280
+ const result = await this.query<Record<string, { nodes: Record<string, unknown>[] }>>(query, variables)
281
+
282
+ const nodes = result[queryName]?.nodes ?? []
283
+ const data = nodes.map(node => transformNativeRecord(node, linkFields, meta, allMeta))
284
+
285
+ return {
286
+ data,
287
+ hasMore: nodes.length === options?.limit,
288
+ }
289
+ }
290
+
203
291
  /**
204
292
  * Execute a doctype action
205
293
  * @param doctype - Doctype reference (name and optional slug)