@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/README.md +154 -81
- package/dist/client.js +87 -6
- package/dist/graphql-client.d.ts +48 -8
- package/dist/graphql-client.js +4884 -1563
- package/dist/graphql-client.js.map +1 -1
- package/dist/graphql-client.umd.cjs +199 -0
- package/dist/graphql-client.umd.cjs.map +1 -0
- package/dist/index.js +1 -0
- package/dist/query.js +231 -0
- package/dist/src/client.d.ts +12 -9
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +221 -0
- package/dist/src/gql/schema.js +53 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +61 -0
- package/dist/src/queries.js +19 -0
- package/dist/src/query.d.ts +35 -0
- package/dist/src/query.d.ts.map +1 -0
- package/dist/src/stonecrop-client.d.ts +87 -0
- package/dist/src/stonecrop-client.d.ts.map +1 -0
- package/dist/src/tsdoc-metadata.json +11 -0
- package/dist/src/types/index.js +4 -0
- package/dist/stonecrop-client.js +191 -0
- package/package.json +6 -3
- package/src/client.ts +124 -16
- package/src/index.ts +1 -0
- package/src/query.ts +303 -0
package/dist/index.js
CHANGED
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
|
+
}
|
package/dist/src/client.d.ts
CHANGED
|
@@ -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)
|
package/dist/src/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
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;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/src/index.d.ts.map
CHANGED
|
@@ -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 };
|