@stonecrop/graphql-client 0.10.16 → 0.11.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/dist/index.js CHANGED
@@ -59,3 +59,4 @@ const methods = {
59
59
  };
60
60
  export { queries, typeDefs, methods };
61
61
  export { StonecropClient } from './client';
62
+ export { buildRecordQuery, buildListQuery } from './query';
package/dist/query.js ADDED
@@ -0,0 +1,231 @@
1
+ import { toPascalCase } from '@stonecrop/schema';
2
+ import pluralize from 'pluralize';
3
+ /**
4
+ * Default sync limit for many-cardinality links
5
+ */
6
+ const DEFAULT_SYNC_LIMIT = 50;
7
+ /**
8
+ * Field types that are not scalar queryable fields.
9
+ * Link fields are handled separately via sub-selections; relationship fields live in `links`.
10
+ */
11
+ const RELATION_FIELDTYPES = new Set(['Link']);
12
+ /**
13
+ * Build a GraphQL connection query to fetch a list of records.
14
+ *
15
+ * Only declares variables ($limit, $offset, $orderBy) that are actually used,
16
+ * avoiding GraphQL spec violations from unused variable declarations.
17
+ *
18
+ * @param meta - Doctype metadata
19
+ * @param connectionFieldName - Function to derive the connection field name from a table name
20
+ * @param orderByTypeName - Function to derive the order-by type name from a table name
21
+ * @param options - Query options (limit, offset, orderBy)
22
+ * @returns GraphQL query string
23
+ *
24
+ * @public
25
+ */
26
+ export function buildListQuery(meta, connectionFieldName, orderByTypeName, options) {
27
+ const fieldNames = queryableFieldNames(meta);
28
+ const connectionName = connectionFieldName(meta.tableName);
29
+ const orderByType = orderByTypeName(meta.tableName);
30
+ const varDecls = [];
31
+ const queryArgs = [];
32
+ if (options?.limit) {
33
+ varDecls.push('$limit: Int');
34
+ queryArgs.push(`first: $limit`);
35
+ }
36
+ if (options?.offset) {
37
+ varDecls.push('$offset: Int');
38
+ queryArgs.push(`offset: $offset`);
39
+ }
40
+ if (options?.orderBy) {
41
+ varDecls.push(`$orderBy: [${orderByType}!]`);
42
+ queryArgs.push(`orderBy: $orderBy`);
43
+ }
44
+ const varStr = varDecls.length > 0 ? `(${varDecls.join(', ')})` : '';
45
+ const argsStr = queryArgs.length > 0 ? `(${queryArgs.join(', ')})` : '';
46
+ return `
47
+ query GetRecords${varStr} {
48
+ ${connectionName}${argsStr} {
49
+ nodes {
50
+ ${fieldNames}
51
+ }
52
+ }
53
+ }
54
+ `;
55
+ }
56
+ /**
57
+ * Build a GraphQL query string from doctype metadata.
58
+ *
59
+ * Generates scalar field selections. When `includeNested` is set,
60
+ * recursively includes descendant link sub-selections derived from
61
+ * the doctype's `links` object.
62
+ *
63
+ * @param meta - Doctype metadata to build the query from
64
+ * @param recordFieldName - Function to derive the query field name from a table name
65
+ * @param recordArgName - Function to derive the argument name from a table name
66
+ * @param recordArgType - Function to derive the argument type from a table name
67
+ * @param registry - Doctype registry for resolving link targets. Required when includeNested is set.
68
+ * @param options - Query options (includeNested, maxDepth)
69
+ * @returns GraphQL query string
70
+ *
71
+ * @public
72
+ */
73
+ export function buildRecordQuery(meta, recordFieldName, recordArgName, recordArgType, registry, options) {
74
+ const queryName = recordFieldName(meta.tableName);
75
+ const argName = recordArgName(meta.tableName);
76
+ const argType = recordArgType(meta.tableName);
77
+ const seen = new Set([meta.slug || meta.name]);
78
+ let selection = queryableFieldNames(meta);
79
+ if (options?.includeNested && meta.links && registry) {
80
+ const includeSet = Array.isArray(options.includeNested) ? new Set(options.includeNested) : null;
81
+ const nestedSelections = buildNestedSelections(meta.links, meta.tableName, includeSet, registry, seen, 0, options.maxDepth);
82
+ if (nestedSelections) {
83
+ selection += '\n ' + nestedSelections;
84
+ }
85
+ }
86
+ return `
87
+ query GetRecord($${argName}: ${argType}) {
88
+ ${queryName}(${argName}: $${argName}) {
89
+ ${selection}
90
+ }
91
+ }
92
+ `;
93
+ }
94
+ /**
95
+ * Build nested sub-selections for descendant links
96
+ * @internal
97
+ */
98
+ function buildNestedSelections(links, parentTableName, includeSet, registry, seen, depth, maxDepth) {
99
+ if (maxDepth !== undefined && depth >= maxDepth)
100
+ return '';
101
+ const selections = [];
102
+ for (const [fieldname, link] of Object.entries(links)) {
103
+ if (maxDepth !== undefined && depth >= maxDepth)
104
+ break;
105
+ // Check blockWorkflows first - if true, it overrides the includeSet filter
106
+ const effectiveBlockWorkflows = getEffectiveBlockWorkflows(link);
107
+ const linkBlockWorkflowsExplicitTrue = link.blockWorkflows === true;
108
+ // Check includeSet filter - but blockWorkflows: true bypasses this filter
109
+ if (includeSet && !includeSet.has(fieldname) && !linkBlockWorkflowsExplicitTrue) {
110
+ continue;
111
+ }
112
+ // Check fetch strategy - skip if not sync (unless blockWorkflows overrides)
113
+ const effectiveFetch = getEffectiveFetchStrategy(link);
114
+ const shouldSkip = effectiveBlockWorkflows === false || (effectiveFetch.method !== 'sync' && !linkBlockWorkflowsExplicitTrue);
115
+ if (shouldSkip) {
116
+ continue;
117
+ }
118
+ // TODO: When blockWorkflows is true with custom fetch, this currently forces the link into
119
+ // GraphQL queries, bypassing the custom handler. This is a workaround — custom handlers
120
+ // should be able to satisfy blockWorkflows on their own schedule. Future enhancement:
121
+ // - Option: Add SchemaValidator error for custom + blockWorkflows (blocking is impossible)
122
+ // - Option: Track pending custom fetches and only unblock when all custom handlers complete
123
+ // See: relationships.md Phase 6 "Open Question: blockWorkflows + custom fetch"
124
+ const targetMeta = registry.get(link.target);
125
+ if (!targetMeta)
126
+ continue;
127
+ const alreadySeen = seen.has(link.target);
128
+ if (alreadySeen) {
129
+ // Self-referential: include scalar fields only, don't modify seen
130
+ }
131
+ else {
132
+ seen.add(link.target);
133
+ }
134
+ const scalarFields = queryableFieldNames(targetMeta);
135
+ let nestedLinks = '';
136
+ if (!alreadySeen && targetMeta.links && targetMeta.tableName && (maxDepth === undefined || depth + 1 < maxDepth)) {
137
+ const innerSelections = buildNestedSelections(targetMeta.links, targetMeta.tableName, null, registry, seen, depth + 1, maxDepth);
138
+ if (innerSelections) {
139
+ nestedLinks = '\n ' + innerSelections;
140
+ }
141
+ seen.delete(link.target);
142
+ }
143
+ const fullSelection = scalarFields + nestedLinks;
144
+ if (isManyCardinality(link.cardinality)) {
145
+ const connectionField = getConnectionFieldName(targetMeta, parentTableName);
146
+ const limitArg = effectiveFetch.method === 'sync' && effectiveFetch.limit !== undefined
147
+ ? `first: ${effectiveFetch.limit}`
148
+ : effectiveFetch.method === 'sync'
149
+ ? `first: ${DEFAULT_SYNC_LIMIT}`
150
+ : '';
151
+ selections.push(`
152
+ ${connectionField}${limitArg ? `(${limitArg})` : ''} {
153
+ nodes {
154
+ ${fullSelection}
155
+ }
156
+ }`);
157
+ }
158
+ else {
159
+ selections.push(`
160
+ ${fieldname} {
161
+ ${fullSelection}
162
+ }`);
163
+ }
164
+ }
165
+ return selections.join('');
166
+ }
167
+ /**
168
+ * Get the effective fetch strategy for a link, applying cardinality-based defaults.
169
+ *
170
+ * - `fetch` explicitly set → use it
171
+ * - `fetch` absent → apply defaults:
172
+ * - `noneOrMany`/`atLeastOne` → `{ method: 'sync', limit: 50 }`
173
+ * - `one`/`atMostOne` → `{ method: 'lazy' }`
174
+ *
175
+ * @internal
176
+ */
177
+ function getEffectiveFetchStrategy(link) {
178
+ if (link.fetch !== undefined) {
179
+ return link.fetch;
180
+ }
181
+ // Apply cardinality-based defaults
182
+ if (isManyCardinality(link.cardinality)) {
183
+ return { method: 'sync', limit: DEFAULT_SYNC_LIMIT };
184
+ }
185
+ else {
186
+ return { method: 'lazy' };
187
+ }
188
+ }
189
+ /**
190
+ * Get the effective blockWorkflows value for a link.
191
+ * Returns true if blockWorkflows is explicitly true, or if it's absent and fetch method is 'sync'.
192
+ * @internal
193
+ */
194
+ function getEffectiveBlockWorkflows(link) {
195
+ if (link.blockWorkflows !== undefined) {
196
+ return link.blockWorkflows;
197
+ }
198
+ const effectiveFetch = getEffectiveFetchStrategy(link);
199
+ return effectiveFetch.method === 'sync';
200
+ }
201
+ /**
202
+ * Get scalar field names for a doctype, excluding Link and Doctype fields
203
+ * @internal
204
+ */
205
+ function queryableFieldNames(meta) {
206
+ return meta.fields
207
+ .filter(f => !RELATION_FIELDTYPES.has(f.fieldtype))
208
+ .map(f => f.fieldname)
209
+ .join('\n ');
210
+ }
211
+ /**
212
+ * Check if a cardinality value represents a 1:many relationship
213
+ * @internal
214
+ */
215
+ function isManyCardinality(cardinality) {
216
+ return cardinality === 'noneOrMany' || cardinality === 'atLeastOne';
217
+ }
218
+ /**
219
+ * Derive a PostGraphile connection field name from a target doctype and parent table name.
220
+ *
221
+ * PostGraphile convention: `{targetPlural}By{ParentTablePascal}Id`
222
+ * Example: recipe_task with parent recipe → `recipeTasksByRecipeId`
223
+ *
224
+ * @internal
225
+ */
226
+ function getConnectionFieldName(targetMeta, parentTableName) {
227
+ const targetPlural = pluralize.plural(targetMeta.tableName);
228
+ const targetPascal = toPascalCase(targetPlural);
229
+ const fkPascal = toPascalCase(parentTableName) + 'Id';
230
+ return `${targetPascal}By${fkPascal}`;
231
+ }
@@ -1,4 +1,4 @@
1
- import type { DataClient, DoctypeMeta, DoctypeContext, DoctypeRef } from '@stonecrop/schema';
1
+ import type { DataClient, DoctypeMeta, DoctypeContext, DoctypeRef, GetRecordOptions, GetRecordsOptions } from '@stonecrop/schema';
2
2
  export type { DoctypeContext, DoctypeRef };
3
3
  /**
4
4
  * Options for creating a Stonecrop client
@@ -9,6 +9,8 @@ export interface StonecropClientOptions {
9
9
  endpoint: string;
10
10
  /** Additional HTTP headers to include in requests */
11
11
  headers?: Record<string, string>;
12
+ /** Doctype registry for nested query building */
13
+ registry?: Map<string, DoctypeMeta>;
12
14
  }
13
15
  /**
14
16
  * Client for interacting with Stonecrop GraphQL API
@@ -18,6 +20,7 @@ export declare class StonecropClient implements DataClient {
18
20
  private endpoint;
19
21
  private headers;
20
22
  private metaCache;
23
+ private registry?;
21
24
  constructor(options: StonecropClientOptions);
22
25
  /**
23
26
  * Execute a GraphQL query
@@ -41,22 +44,22 @@ export declare class StonecropClient implements DataClient {
41
44
  */
42
45
  getAllMeta(): Promise<DoctypeMeta[]>;
43
46
  /**
44
- * Get a single record by ID
47
+ * Get a single record by ID.
48
+ *
49
+ * When `includeNested` is set, builds a query with sub-selections for descendant
50
+ * links and returns ancestor + merged descendants. When omitted, returns flat scalar data.
51
+ *
45
52
  * @param doctype - Doctype reference (name and optional slug)
46
53
  * @param recordId - Record ID to fetch
54
+ * @param options - Query options (includeNested, maxDepth)
47
55
  */
48
- getRecord(doctype: DoctypeRef, recordId: string): Promise<Record<string, unknown> | null>;
56
+ getRecord(doctype: DoctypeRef, recordId: string, options?: GetRecordOptions): Promise<Record<string, unknown> | null>;
49
57
  /**
50
58
  * Get multiple records with optional filtering and pagination
51
59
  * @param doctype - Doctype reference (name and optional slug)
52
60
  * @param options - Query options (filters, orderBy, limit, offset)
53
61
  */
54
- getRecords(doctype: DoctypeRef, options?: {
55
- filters?: Record<string, unknown>;
56
- orderBy?: string;
57
- limit?: number;
58
- offset?: number;
59
- }): Promise<Record<string, unknown>[]>;
62
+ getRecords(doctype: DoctypeRef, options?: GetRecordsOptions): Promise<Record<string, unknown>[]>;
60
63
  /**
61
64
  * Execute a doctype action
62
65
  * @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,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAE5F,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAA;AAE1C;;;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;;;GAGG;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;;;;OAIG;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;IAmBxF;;;;OAIG;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;IAyDnE;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAqD1C;;;;OAIG;IACG,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAiB/F;;;;OAIG;IACG,UAAU,CACf,OAAO,EAAE,UAAU,EACnB,OAAO,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjC,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,KAAK,CAAC,EAAE,MAAM,CAAA;QACd,MAAM,CAAC,EAAE,MAAM,CAAA;KACf,GACC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAiCrC;;;;;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;IAuBrE;;OAEG;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,MAAM,mBAAmB,CAAA;AAM1B,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAA;AAe1C;;;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;IAChC,iDAAiD;IACjD,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;CACnC;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,UAAU;IACjD,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,SAAS,CAAsC;IACvD,OAAO,CAAC,QAAQ,CAAC,CAA0B;gBAE/B,OAAO,EAAE,sBAAsB;IAS3C;;;;OAIG;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;IAmBxF;;;;OAIG;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;IAuDnE;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAmD1C;;;;;;;;;OASG;IACG,SAAS,CACd,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IA8C1C;;;;OAIG;IACG,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAiCtG;;;;;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;IAuBrE;;OAEG;IACH,cAAc,IAAI,IAAI;CAGtB"}
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Client for interacting with Stonecrop GraphQL API
3
+ * @public
4
+ */
5
+ export class StonecropClient {
6
+ endpoint;
7
+ headers;
8
+ metaCache = new Map();
9
+ constructor(options) {
10
+ this.endpoint = options.endpoint;
11
+ this.headers = {
12
+ 'Content-Type': 'application/json',
13
+ ...options.headers,
14
+ };
15
+ }
16
+ /**
17
+ * Execute a GraphQL query
18
+ * @param query - GraphQL query string
19
+ * @param variables - Query variables
20
+ */
21
+ async query(query, variables) {
22
+ const response = await fetch(this.endpoint, {
23
+ method: 'POST',
24
+ headers: this.headers,
25
+ body: JSON.stringify({ query, variables }),
26
+ });
27
+ const json = (await response.json());
28
+ if (json.errors?.length) {
29
+ throw new Error(json.errors[0].message);
30
+ }
31
+ return json.data;
32
+ }
33
+ /**
34
+ * Execute a GraphQL mutation
35
+ * @param mutation - GraphQL mutation string
36
+ * @param variables - Mutation variables
37
+ */
38
+ async mutate(mutation, variables) {
39
+ return this.query(mutation, variables);
40
+ }
41
+ /**
42
+ * Get doctype metadata
43
+ * @param context - Doctype context containing doctype name
44
+ */
45
+ async getMeta(context) {
46
+ const cached = this.metaCache.get(context.doctype);
47
+ if (cached)
48
+ return cached;
49
+ const result = await this.query(`
50
+ query GetMeta($doctype: String!) {
51
+ stonecropMeta(doctype: $doctype) {
52
+ name
53
+ slug
54
+ tableName
55
+ fields {
56
+ fieldname
57
+ fieldtype
58
+ component
59
+ label
60
+ width
61
+ align
62
+ required
63
+ readOnly
64
+ edit
65
+ hidden
66
+ default
67
+ options
68
+ mask
69
+ precision
70
+ scale
71
+ mode
72
+ validation
73
+ }
74
+ workflow {
75
+ states
76
+ actions {
77
+ label
78
+ handler
79
+ requiredFields
80
+ allowedStates
81
+ confirm
82
+ args
83
+ }
84
+ }
85
+ inherits
86
+ listDoctype
87
+ parentDoctype
88
+ }
89
+ }
90
+ `, { doctype: context.doctype });
91
+ if (result.stonecropMeta) {
92
+ this.metaCache.set(context.doctype, result.stonecropMeta);
93
+ }
94
+ return result.stonecropMeta;
95
+ }
96
+ /**
97
+ * Get all doctype metadata
98
+ */
99
+ async getAllMeta() {
100
+ const result = await this.query(`
101
+ query GetAllMeta {
102
+ stonecropAllMeta {
103
+ name
104
+ slug
105
+ tableName
106
+ fields {
107
+ fieldname
108
+ fieldtype
109
+ component
110
+ label
111
+ width
112
+ align
113
+ required
114
+ readOnly
115
+ edit
116
+ hidden
117
+ default
118
+ options
119
+ mask
120
+ precision
121
+ scale
122
+ mode
123
+ validation
124
+ }
125
+ workflow {
126
+ states
127
+ actions {
128
+ label
129
+ handler
130
+ requiredFields
131
+ allowedStates
132
+ confirm
133
+ args
134
+ }
135
+ }
136
+ inherits
137
+ listDoctype
138
+ parentDoctype
139
+ }
140
+ }
141
+ `);
142
+ for (const meta of result.stonecropAllMeta) {
143
+ this.metaCache.set(meta.name, meta);
144
+ }
145
+ return result.stonecropAllMeta;
146
+ }
147
+ /**
148
+ * Get a single record by ID
149
+ * @param doctype - Doctype reference (name and optional slug)
150
+ * @param recordId - Record ID to fetch
151
+ */
152
+ async getRecord(doctype, recordId) {
153
+ const result = await this.query(`
154
+ query GetRecord($doctype: String!, $id: String!) {
155
+ stonecropRecord(doctype: $doctype, id: $id) {
156
+ data
157
+ }
158
+ }
159
+ `, { doctype: doctype.name, id: recordId });
160
+ return result.stonecropRecord.data;
161
+ }
162
+ /**
163
+ * Get multiple records with optional filtering and pagination
164
+ * @param doctype - Doctype reference (name and optional slug)
165
+ * @param options - Query options (filters, orderBy, limit, offset)
166
+ */
167
+ async getRecords(doctype, options) {
168
+ const result = await this.query(`
169
+ query GetRecords(
170
+ $doctype: String!
171
+ $filters: JSON
172
+ $orderBy: String
173
+ $limit: Int
174
+ $offset: Int
175
+ ) {
176
+ stonecropRecords(
177
+ doctype: $doctype
178
+ filters: $filters
179
+ orderBy: $orderBy
180
+ limit: $limit
181
+ offset: $offset
182
+ ) {
183
+ data
184
+ count
185
+ }
186
+ }
187
+ `, {
188
+ doctype: doctype.name,
189
+ ...options,
190
+ });
191
+ return result.stonecropRecords.data;
192
+ }
193
+ /**
194
+ * Execute a doctype action
195
+ * @param doctype - Doctype reference (name and optional slug)
196
+ * @param action - Action name to execute
197
+ * @param args - Action arguments
198
+ */
199
+ async runAction(doctype, action, args) {
200
+ const result = await this.query(`
201
+ mutation RunAction($doctype: String!, $action: String!, $args: JSON) {
202
+ stonecropAction(doctype: $doctype, action: $action, args: $args) {
203
+ success
204
+ data
205
+ error
206
+ }
207
+ }
208
+ `, {
209
+ doctype: doctype.name,
210
+ action,
211
+ args,
212
+ });
213
+ return result.stonecropAction;
214
+ }
215
+ /**
216
+ * Clear the cached doctype metadata
217
+ */
218
+ clearMetaCache() {
219
+ this.metaCache.clear();
220
+ }
221
+ }
@@ -0,0 +1,53 @@
1
+ import { gql } from 'graphql-request';
2
+ /**
3
+ * This is the schema for the GraphQL API.
4
+ * @public
5
+ */
6
+ const typeDefs = gql `
7
+ type Doctype {
8
+ id: ID!
9
+ name: String!
10
+ workflow: String!
11
+ schema: String!
12
+ actions: String!
13
+ }
14
+
15
+ type DoctypeField {
16
+ id: ID!
17
+ label: String!
18
+ fieldtype: String
19
+ component: String
20
+ required: Boolean
21
+ readonly: Boolean
22
+ }
23
+
24
+ type DoctypeWorkflow {
25
+ name: String!
26
+ machine: StateMachine!
27
+ }
28
+
29
+ type StateMachine {
30
+ id: ID!
31
+ }
32
+
33
+ type DoctypeAction {
34
+ eventName: String!
35
+ callback: String
36
+ }
37
+
38
+ type Query {
39
+ getMeta(doctype: String!): Doctype # ∪ error
40
+ getRecords(doctype: String!, filters: [String]): [String] # ∪ error
41
+ getRecord(doctype: String!, id: ID!): String # ∪ error
42
+ }
43
+
44
+ type Mutation {
45
+ runAction(doctype: String!, id: [ID!]!, functionName: String!): [String!]! # ∪ error
46
+ }
47
+
48
+ schema {
49
+ query: Query
50
+ mutation: Mutation
51
+ }
52
+ `;
53
+ export default typeDefs;
@@ -13,6 +13,7 @@ declare const methods: {
13
13
  };
14
14
  export { queries, typeDefs, methods };
15
15
  export { StonecropClient } from './client';
16
+ export { buildRecordQuery, buildListQuery } from './query';
16
17
  export type { StonecropClientOptions, DoctypeContext } from './client';
17
18
  export type { Meta, MetaParser, MetaResponse } from './types';
18
19
  export type { DoctypeMeta } from '@stonecrop/schema';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,QAAQ,MAAM,cAAc,CAAA;AACnC,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,SAAS,CAAA;AAkC7D;;;;;;GAMG;AACH,QAAA,MAAM,OAAO;uBACa,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,YAAY,CAAC;CAgBrE,CAAA;AAED,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,YAAY,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AACtE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,QAAQ,MAAM,cAAc,CAAA;AACnC,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,SAAS,CAAA;AAkC7D;;;;;;GAMG;AACH,QAAA,MAAM,OAAO;uBACa,MAAM,QAAQ,MAAM,KAAG,OAAO,CAAC,YAAY,CAAC;CAgBrE,CAAA;AAED,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAC1D,YAAY,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AACtE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA"}
@@ -0,0 +1,61 @@
1
+ import { Decimal } from 'decimal.js';
2
+ import { GraphQLClient } from 'graphql-request';
3
+ import { queries } from './queries';
4
+ import typeDefs from './gql/schema';
5
+ /**
6
+ * Parse the response from the GraphQL server. Converts the stringified JSON to JSON and converts the stringified numbers to Decimal.
7
+ * @param obj - The response from the GraphQL server
8
+ * @returns The parsed response
9
+ * @example
10
+ * const response = '{"data":{"getMeta":{"id":"Issue","name":"Issue","workflow":"{\"machineId\":null,\"name\":\"save\",\"id\":\"1\"}","schema":"[{\"label\":\"Subject\",\"id\":\"1\"}]","actions":"[{\"eventName\":\"save\",\"id\":\"1\"}]"}}}'
11
+ * const parsedResponse = metaParser(response)
12
+ * console.log(parsedResponse)
13
+ * /* Output: {"id": "Issue", "name": "Issue", "workflow": { "machineId": null, "name": "save", "id": "1" }, "schema": [{ "label": "Subject", "id": "1" }], "actions": [{ "eventName": "save", "id": "1" }]}
14
+ */
15
+ const metaParser = (obj) => {
16
+ return JSON.parse(obj, (key, value) => {
17
+ if (typeof value === 'string') {
18
+ try {
19
+ return JSON.parse(value, (_key, value) => {
20
+ if (typeof value === 'string' && !isNaN(Number(value))) {
21
+ return new Decimal(value);
22
+ }
23
+ return value;
24
+ });
25
+ }
26
+ catch {
27
+ // if the value is not a stringified JSON, return as it is
28
+ return value;
29
+ }
30
+ }
31
+ else if (!isNaN(Number(value))) {
32
+ return new Decimal(value);
33
+ }
34
+ return value;
35
+ });
36
+ };
37
+ /**
38
+ * Get meta information for a doctype
39
+ * @param doctype - The doctype to get meta information for
40
+ * @param url - The URL to send the request to
41
+ * @returns The meta information for the doctype
42
+ * @public
43
+ */
44
+ const methods = {
45
+ getMeta: async (doctype, url) => {
46
+ const client = new GraphQLClient(url || '/graphql', {
47
+ fetch: window.fetch,
48
+ jsonSerializer: {
49
+ stringify: obj => JSON.stringify(obj), // process the request object before sending; leave as default JSON
50
+ parse: metaParser, // process the response meta object
51
+ },
52
+ });
53
+ const { getMeta } = await client.request({
54
+ document: queries.getMeta,
55
+ variables: { doctype },
56
+ });
57
+ return getMeta;
58
+ },
59
+ };
60
+ export { queries, typeDefs, methods };
61
+ export { StonecropClient } from './client';
@@ -0,0 +1,19 @@
1
+ import { gql } from 'graphql-request';
2
+ /**
3
+ * Queries for the GraphQL API.
4
+ * @public
5
+ */
6
+ const queries = {
7
+ getMeta: gql `
8
+ query getDoctype($doctype: String!) {
9
+ getMeta(doctype: $doctype) {
10
+ id
11
+ name
12
+ workflow
13
+ schema
14
+ actions
15
+ }
16
+ }
17
+ `,
18
+ };
19
+ export { queries };