@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DoctypeMeta, GetRecordOptions, GetRecordsOptions } from '@stonecrop/schema';
|
|
2
|
+
/**
|
|
3
|
+
* Build a GraphQL connection query to fetch a list of records.
|
|
4
|
+
*
|
|
5
|
+
* Only declares variables ($limit, $offset, $orderBy) that are actually used,
|
|
6
|
+
* avoiding GraphQL spec violations from unused variable declarations.
|
|
7
|
+
*
|
|
8
|
+
* @param meta - Doctype metadata
|
|
9
|
+
* @param connectionFieldName - Function to derive the connection field name from a table name
|
|
10
|
+
* @param orderByTypeName - Function to derive the order-by type name from a table name
|
|
11
|
+
* @param options - Query options (limit, offset, orderBy)
|
|
12
|
+
* @returns GraphQL query string
|
|
13
|
+
*
|
|
14
|
+
* @public
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildListQuery(meta: DoctypeMeta, connectionFieldName: (t: string) => string, orderByTypeName: (t: string) => string, options?: GetRecordsOptions): string;
|
|
17
|
+
/**
|
|
18
|
+
* Build a GraphQL query string from doctype metadata.
|
|
19
|
+
*
|
|
20
|
+
* Generates scalar field selections. When `includeNested` is set,
|
|
21
|
+
* recursively includes descendant link sub-selections derived from
|
|
22
|
+
* the doctype's `links` object.
|
|
23
|
+
*
|
|
24
|
+
* @param meta - Doctype metadata to build the query from
|
|
25
|
+
* @param recordFieldName - Function to derive the query field name from a table name
|
|
26
|
+
* @param recordArgName - Function to derive the argument name from a table name
|
|
27
|
+
* @param recordArgType - Function to derive the argument type from a table name
|
|
28
|
+
* @param registry - Doctype registry for resolving link targets. Required when includeNested is set.
|
|
29
|
+
* @param options - Query options (includeNested, maxDepth)
|
|
30
|
+
* @returns GraphQL query string
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildRecordQuery(meta: DoctypeMeta, recordFieldName: (t: string) => string, recordArgName: (t: string) => string, recordArgType: (t: string) => string, registry?: Map<string, DoctypeMeta>, options?: GetRecordOptions): string;
|
|
35
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/query.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,WAAW,EACX,gBAAgB,EAChB,iBAAiB,EAIjB,MAAM,mBAAmB,CAAA;AAe1B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC7B,IAAI,EAAE,WAAW,EACjB,mBAAmB,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAC1C,eAAe,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EACtC,OAAO,CAAC,EAAE,iBAAiB,GACzB,MAAM,CAgCR;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,WAAW,EACjB,eAAe,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EACtC,aAAa,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EACpC,aAAa,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EACpC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EACnC,OAAO,CAAC,EAAE,gBAAgB,GACxB,MAAM,CAkCR"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { DoctypeMeta as DoctypeMetaType } from '@stonecrop/schema';
|
|
2
|
+
/**
|
|
3
|
+
* Route context for identifying what doctype/record we're working with
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export interface RouteContext {
|
|
7
|
+
/** Doctype name (e.g., 'Task', 'Customer') */
|
|
8
|
+
doctype: string;
|
|
9
|
+
/** Optional record ID for viewing/editing a specific record */
|
|
10
|
+
recordId?: string;
|
|
11
|
+
/** Additional context properties */
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Options for creating a Stonecrop client
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export interface StonecropClientOptions {
|
|
19
|
+
/** GraphQL endpoint URL */
|
|
20
|
+
endpoint: string;
|
|
21
|
+
/** Additional HTTP headers to include in requests */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Client for interacting with Stonecrop GraphQL API
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
export declare class StonecropClient {
|
|
29
|
+
private endpoint;
|
|
30
|
+
private headers;
|
|
31
|
+
private metaCache;
|
|
32
|
+
constructor(options: StonecropClientOptions);
|
|
33
|
+
/**
|
|
34
|
+
* Execute a GraphQL query
|
|
35
|
+
* @param query - GraphQL query string
|
|
36
|
+
* @param variables - Query variables
|
|
37
|
+
*/
|
|
38
|
+
query<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T>;
|
|
39
|
+
/**
|
|
40
|
+
* Execute a GraphQL mutation
|
|
41
|
+
* @param mutation - GraphQL mutation string
|
|
42
|
+
* @param variables - Mutation variables
|
|
43
|
+
*/
|
|
44
|
+
mutate<T = unknown>(mutation: string, variables?: Record<string, unknown>): Promise<T>;
|
|
45
|
+
/**
|
|
46
|
+
* Get doctype metadata
|
|
47
|
+
* @param context - Route context containing doctype name
|
|
48
|
+
*/
|
|
49
|
+
getMeta(context: RouteContext): Promise<DoctypeMetaType | null>;
|
|
50
|
+
/**
|
|
51
|
+
* Get all doctype metadata
|
|
52
|
+
*/
|
|
53
|
+
getAllMeta(): Promise<DoctypeMetaType[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Get a single record by ID
|
|
56
|
+
* @param doctype - Doctype metadata
|
|
57
|
+
* @param recordId - Record ID to fetch
|
|
58
|
+
*/
|
|
59
|
+
getRecord(doctype: DoctypeMetaType, recordId: string): Promise<Record<string, unknown> | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Get multiple records with optional filtering and pagination
|
|
62
|
+
* @param doctype - Doctype metadata
|
|
63
|
+
* @param options - Query options (filters, orderBy, limit, offset)
|
|
64
|
+
*/
|
|
65
|
+
getRecords(doctype: DoctypeMetaType, options?: {
|
|
66
|
+
filters?: Record<string, unknown>;
|
|
67
|
+
orderBy?: string;
|
|
68
|
+
limit?: number;
|
|
69
|
+
offset?: number;
|
|
70
|
+
}): Promise<Record<string, unknown>[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Execute a doctype action
|
|
73
|
+
* @param doctype - Doctype metadata
|
|
74
|
+
* @param action - Action name to execute
|
|
75
|
+
* @param args - Action arguments
|
|
76
|
+
*/
|
|
77
|
+
runAction(doctype: DoctypeMetaType, action: string, args?: unknown[]): Promise<{
|
|
78
|
+
success: boolean;
|
|
79
|
+
data: unknown;
|
|
80
|
+
error: string | null;
|
|
81
|
+
}>;
|
|
82
|
+
/**
|
|
83
|
+
* Clear the cached doctype metadata
|
|
84
|
+
*/
|
|
85
|
+
clearMetaCache(): void;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=stonecrop-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stonecrop-client.d.ts","sourceRoot":"","sources":["../../src/stonecrop-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAEvE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oCAAoC;IACpC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACtB;AAED;;;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,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,SAAS,CAA0C;gBAE/C,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,YAAY,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IA0CrE;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAsC9C;;;;OAIG;IACG,SAAS,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAiBpG;;;;OAIG;IACG,UAAU,CACf,OAAO,EAAE,eAAe,EACxB,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,eAAe,EACxB,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,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.57.0"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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 - Route 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
|
+
required
|
|
61
|
+
readOnly
|
|
62
|
+
options
|
|
63
|
+
precision
|
|
64
|
+
scale
|
|
65
|
+
}
|
|
66
|
+
workflow {
|
|
67
|
+
states
|
|
68
|
+
actions
|
|
69
|
+
}
|
|
70
|
+
inherits
|
|
71
|
+
listDoctype
|
|
72
|
+
parentDoctype
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`, { doctype: context.doctype });
|
|
76
|
+
if (result.stonecropMeta) {
|
|
77
|
+
this.metaCache.set(context.doctype, result.stonecropMeta);
|
|
78
|
+
}
|
|
79
|
+
return result.stonecropMeta;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get all doctype metadata
|
|
83
|
+
*/
|
|
84
|
+
async getAllMeta() {
|
|
85
|
+
const result = await this.query(`
|
|
86
|
+
query GetAllMeta {
|
|
87
|
+
stonecropAllMeta {
|
|
88
|
+
name
|
|
89
|
+
slug
|
|
90
|
+
tableName
|
|
91
|
+
fields {
|
|
92
|
+
fieldname
|
|
93
|
+
fieldtype
|
|
94
|
+
component
|
|
95
|
+
label
|
|
96
|
+
required
|
|
97
|
+
readOnly
|
|
98
|
+
options
|
|
99
|
+
precision
|
|
100
|
+
scale
|
|
101
|
+
}
|
|
102
|
+
workflow {
|
|
103
|
+
states
|
|
104
|
+
actions
|
|
105
|
+
}
|
|
106
|
+
inherits
|
|
107
|
+
listDoctype
|
|
108
|
+
parentDoctype
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
`);
|
|
112
|
+
for (const meta of result.stonecropAllMeta) {
|
|
113
|
+
this.metaCache.set(meta.name, meta);
|
|
114
|
+
}
|
|
115
|
+
return result.stonecropAllMeta;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get a single record by ID
|
|
119
|
+
* @param doctype - Doctype metadata
|
|
120
|
+
* @param recordId - Record ID to fetch
|
|
121
|
+
*/
|
|
122
|
+
async getRecord(doctype, recordId) {
|
|
123
|
+
const result = await this.query(`
|
|
124
|
+
query GetRecord($doctype: String!, $id: String!) {
|
|
125
|
+
stonecropRecord(doctype: $doctype, id: $id) {
|
|
126
|
+
data
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
`, { doctype: doctype.name, id: recordId });
|
|
130
|
+
return result.stonecropRecord.data;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Get multiple records with optional filtering and pagination
|
|
134
|
+
* @param doctype - Doctype metadata
|
|
135
|
+
* @param options - Query options (filters, orderBy, limit, offset)
|
|
136
|
+
*/
|
|
137
|
+
async getRecords(doctype, options) {
|
|
138
|
+
const result = await this.query(`
|
|
139
|
+
query GetRecords(
|
|
140
|
+
$doctype: String!
|
|
141
|
+
$filters: JSON
|
|
142
|
+
$orderBy: String
|
|
143
|
+
$limit: Int
|
|
144
|
+
$offset: Int
|
|
145
|
+
) {
|
|
146
|
+
stonecropRecords(
|
|
147
|
+
doctype: $doctype
|
|
148
|
+
filters: $filters
|
|
149
|
+
orderBy: $orderBy
|
|
150
|
+
limit: $limit
|
|
151
|
+
offset: $offset
|
|
152
|
+
) {
|
|
153
|
+
data
|
|
154
|
+
count
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
`, {
|
|
158
|
+
doctype: doctype.name,
|
|
159
|
+
...options,
|
|
160
|
+
});
|
|
161
|
+
return result.stonecropRecords.data;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Execute a doctype action
|
|
165
|
+
* @param doctype - Doctype metadata
|
|
166
|
+
* @param action - Action name to execute
|
|
167
|
+
* @param args - Action arguments
|
|
168
|
+
*/
|
|
169
|
+
async runAction(doctype, action, args) {
|
|
170
|
+
const result = await this.query(`
|
|
171
|
+
mutation RunAction($doctype: String!, $action: String!, $args: JSON) {
|
|
172
|
+
stonecropAction(doctype: $doctype, action: $action, args: $args) {
|
|
173
|
+
success
|
|
174
|
+
data
|
|
175
|
+
error
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
`, {
|
|
179
|
+
doctype: doctype.name,
|
|
180
|
+
action,
|
|
181
|
+
args,
|
|
182
|
+
});
|
|
183
|
+
return result.stonecropAction;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Clear the cached doctype metadata
|
|
187
|
+
*/
|
|
188
|
+
clearMetaCache() {
|
|
189
|
+
this.metaCache.clear();
|
|
190
|
+
}
|
|
191
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/graphql-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -33,14 +33,17 @@
|
|
|
33
33
|
"decimal.js": "^10.6.0",
|
|
34
34
|
"graphql": "^16.12.0",
|
|
35
35
|
"graphql-request": "^7.4.0",
|
|
36
|
-
"
|
|
37
|
-
"@stonecrop/
|
|
36
|
+
"pluralize": "^8.0.0",
|
|
37
|
+
"@stonecrop/schema": "0.11.0",
|
|
38
|
+
"@stonecrop/stonecrop": "0.11.0"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@eslint/js": "^9.39.2",
|
|
41
42
|
"@microsoft/api-documenter": "^7.28.2",
|
|
42
43
|
"@miragejs/graphql": "^0.1.13",
|
|
43
44
|
"@rushstack/heft": "^1.2.0",
|
|
45
|
+
"@types/node": "^22.19.5",
|
|
46
|
+
"@types/pluralize": "^0.0.33",
|
|
44
47
|
"@vitejs/plugin-vue": "^6.0.3",
|
|
45
48
|
"@vitest/coverage-istanbul": "^4.0.18",
|
|
46
49
|
"eslint": "^9.39.2",
|
package/src/client.ts
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DataClient,
|
|
3
|
+
DoctypeMeta,
|
|
4
|
+
DoctypeContext,
|
|
5
|
+
DoctypeRef,
|
|
6
|
+
GetRecordOptions,
|
|
7
|
+
GetRecordsOptions,
|
|
8
|
+
} from '@stonecrop/schema'
|
|
9
|
+
import { snakeToCamel, toPascalCase } from '@stonecrop/schema'
|
|
10
|
+
import pluralize from 'pluralize'
|
|
11
|
+
|
|
12
|
+
import { buildRecordQuery } from './query'
|
|
2
13
|
|
|
3
14
|
export type { DoctypeContext, DoctypeRef }
|
|
4
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Default inflection functions for PostGraphile Amber preset conventions.
|
|
18
|
+
* These match the middleware's default inflection so the client builds
|
|
19
|
+
* queries the server can execute.
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
const defaultRecordFieldName = (tableName: string): string => {
|
|
23
|
+
const singularName = pluralize.singular(tableName)
|
|
24
|
+
return `${snakeToCamel(singularName)}ById`
|
|
25
|
+
}
|
|
26
|
+
const defaultRecordArgName = (_tableName: string): string => 'id'
|
|
27
|
+
const defaultRecordArgType = (_tableName: string): string => 'UUID!'
|
|
28
|
+
|
|
5
29
|
/**
|
|
6
30
|
* Options for creating a Stonecrop client
|
|
7
31
|
* @public
|
|
@@ -11,6 +35,8 @@ export interface StonecropClientOptions {
|
|
|
11
35
|
endpoint: string
|
|
12
36
|
/** Additional HTTP headers to include in requests */
|
|
13
37
|
headers?: Record<string, string>
|
|
38
|
+
/** Doctype registry for nested query building */
|
|
39
|
+
registry?: Map<string, DoctypeMeta>
|
|
14
40
|
}
|
|
15
41
|
|
|
16
42
|
/**
|
|
@@ -21,6 +47,7 @@ export class StonecropClient implements DataClient {
|
|
|
21
47
|
private endpoint: string
|
|
22
48
|
private headers: Record<string, string>
|
|
23
49
|
private metaCache: Map<string, DoctypeMeta> = new Map()
|
|
50
|
+
private registry?: Map<string, DoctypeMeta>
|
|
24
51
|
|
|
25
52
|
constructor(options: StonecropClientOptions) {
|
|
26
53
|
this.endpoint = options.endpoint
|
|
@@ -28,6 +55,7 @@ export class StonecropClient implements DataClient {
|
|
|
28
55
|
'Content-Type': 'application/json',
|
|
29
56
|
...options.headers,
|
|
30
57
|
}
|
|
58
|
+
this.registry = options.registry
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
/**
|
|
@@ -109,8 +137,6 @@ export class StonecropClient implements DataClient {
|
|
|
109
137
|
}
|
|
110
138
|
}
|
|
111
139
|
inherits
|
|
112
|
-
listDoctype
|
|
113
|
-
parentDoctype
|
|
114
140
|
}
|
|
115
141
|
}
|
|
116
142
|
`,
|
|
@@ -166,8 +192,6 @@ export class StonecropClient implements DataClient {
|
|
|
166
192
|
}
|
|
167
193
|
}
|
|
168
194
|
inherits
|
|
169
|
-
listDoctype
|
|
170
|
-
parentDoctype
|
|
171
195
|
}
|
|
172
196
|
}
|
|
173
197
|
`
|
|
@@ -181,11 +205,49 @@ export class StonecropClient implements DataClient {
|
|
|
181
205
|
}
|
|
182
206
|
|
|
183
207
|
/**
|
|
184
|
-
* Get a single record by ID
|
|
208
|
+
* Get a single record by ID.
|
|
209
|
+
*
|
|
210
|
+
* When `includeNested` is set, builds a query with sub-selections for descendant
|
|
211
|
+
* links and returns ancestor + merged descendants. When omitted, returns flat scalar data.
|
|
212
|
+
*
|
|
185
213
|
* @param doctype - Doctype reference (name and optional slug)
|
|
186
214
|
* @param recordId - Record ID to fetch
|
|
215
|
+
* @param options - Query options (includeNested, maxDepth)
|
|
187
216
|
*/
|
|
188
|
-
async getRecord(
|
|
217
|
+
async getRecord(
|
|
218
|
+
doctype: DoctypeRef,
|
|
219
|
+
recordId: string,
|
|
220
|
+
options?: GetRecordOptions
|
|
221
|
+
): Promise<Record<string, unknown> | null> {
|
|
222
|
+
// Nested path: build query with sub-selections
|
|
223
|
+
if (options?.includeNested) {
|
|
224
|
+
const meta = await this.getMeta({ doctype: doctype.name })
|
|
225
|
+
if (!meta) return null
|
|
226
|
+
|
|
227
|
+
const query = buildRecordQuery(
|
|
228
|
+
meta,
|
|
229
|
+
defaultRecordFieldName,
|
|
230
|
+
defaultRecordArgName,
|
|
231
|
+
defaultRecordArgType,
|
|
232
|
+
this.registry,
|
|
233
|
+
options
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
const result = await this.query<Record<string, unknown>>(query, { id: recordId })
|
|
237
|
+
|
|
238
|
+
const queryName = defaultRecordFieldName(meta.tableName || doctype.name)
|
|
239
|
+
const record = result[queryName] as Record<string, unknown> | undefined
|
|
240
|
+
|
|
241
|
+
if (!record) return null
|
|
242
|
+
|
|
243
|
+
if (meta.links && this.registry) {
|
|
244
|
+
return mergeNestedResults(record, meta, this.registry)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return record
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Flat path: original query
|
|
189
251
|
const result = await this.query<{
|
|
190
252
|
stonecropRecord: { data: Record<string, unknown> | null }
|
|
191
253
|
}>(
|
|
@@ -207,15 +269,7 @@ export class StonecropClient implements DataClient {
|
|
|
207
269
|
* @param doctype - Doctype reference (name and optional slug)
|
|
208
270
|
* @param options - Query options (filters, orderBy, limit, offset)
|
|
209
271
|
*/
|
|
210
|
-
async getRecords(
|
|
211
|
-
doctype: DoctypeRef,
|
|
212
|
-
options?: {
|
|
213
|
-
filters?: Record<string, unknown>
|
|
214
|
-
orderBy?: string
|
|
215
|
-
limit?: number
|
|
216
|
-
offset?: number
|
|
217
|
-
}
|
|
218
|
-
): Promise<Record<string, unknown>[]> {
|
|
272
|
+
async getRecords(doctype: DoctypeRef, options?: GetRecordsOptions): Promise<Record<string, unknown>[]> {
|
|
219
273
|
const result = await this.query<{
|
|
220
274
|
stonecropRecords: { data: Record<string, unknown>[] }
|
|
221
275
|
}>(
|
|
@@ -288,3 +342,57 @@ export class StonecropClient implements DataClient {
|
|
|
288
342
|
this.metaCache.clear()
|
|
289
343
|
}
|
|
290
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Merge nested connection results into flat arrays.
|
|
348
|
+
*
|
|
349
|
+
* For `noneOrMany`/`atLeastOne` links, the query returns `{ nodes: [...] }`.
|
|
350
|
+
* This flattens them to just `[]` for easier consumption.
|
|
351
|
+
*
|
|
352
|
+
* For `one`/`atMostOne` links, the result is already flat.
|
|
353
|
+
*
|
|
354
|
+
* @internal
|
|
355
|
+
*/
|
|
356
|
+
function mergeNestedResults(
|
|
357
|
+
record: Record<string, unknown>,
|
|
358
|
+
meta: DoctypeMeta,
|
|
359
|
+
registry: Map<string, DoctypeMeta>
|
|
360
|
+
): Record<string, unknown> {
|
|
361
|
+
if (!meta.links) return record
|
|
362
|
+
|
|
363
|
+
const merged = { ...record }
|
|
364
|
+
|
|
365
|
+
for (const [fieldname, link] of Object.entries(meta.links)) {
|
|
366
|
+
const isMany = link.cardinality === 'noneOrMany' || link.cardinality === 'atLeastOne'
|
|
367
|
+
|
|
368
|
+
if (isMany) {
|
|
369
|
+
// Connection result: { nodes: [...] } → flatten to []
|
|
370
|
+
const targetMeta = registry.get(link.target)
|
|
371
|
+
if (!targetMeta) continue
|
|
372
|
+
|
|
373
|
+
const connectionField = getConnectionFieldFromTarget(targetMeta, meta.tableName || '')
|
|
374
|
+
const connectionResult = merged[connectionField] as { nodes?: unknown[] } | undefined
|
|
375
|
+
if (connectionResult?.nodes) {
|
|
376
|
+
merged[fieldname] = connectionResult.nodes
|
|
377
|
+
delete merged[connectionField]
|
|
378
|
+
} else {
|
|
379
|
+
merged[fieldname] = []
|
|
380
|
+
delete merged[connectionField]
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// 'one'/'atMostOne' links are already at the right fieldname
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return merged
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Derive the connection field name matching the query builder's convention.
|
|
391
|
+
* @internal
|
|
392
|
+
*/
|
|
393
|
+
function getConnectionFieldFromTarget(targetMeta: DoctypeMeta, parentTableName: string): string {
|
|
394
|
+
const targetPlural = pluralize.plural(targetMeta.tableName || '')
|
|
395
|
+
const targetPascal = toPascalCase(targetPlural)
|
|
396
|
+
const fkPascal = toPascalCase(parentTableName) + 'Id'
|
|
397
|
+
return `${targetPascal}By${fkPascal}`
|
|
398
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -65,6 +65,7 @@ const methods = {
|
|
|
65
65
|
|
|
66
66
|
export { queries, typeDefs, methods }
|
|
67
67
|
export { StonecropClient } from './client'
|
|
68
|
+
export { buildRecordQuery, buildListQuery } from './query'
|
|
68
69
|
export type { StonecropClientOptions, DoctypeContext } from './client'
|
|
69
70
|
export type { Meta, MetaParser, MetaResponse } from './types'
|
|
70
71
|
export type { DoctypeMeta } from '@stonecrop/schema'
|