@stonecrop/graphql-client 0.11.0 → 0.11.2

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.
Files changed (42) hide show
  1. package/README.md +35 -130
  2. package/dist/client.js +38 -93
  3. package/dist/graphql-client.d.ts +28 -114
  4. package/dist/graphql-client.js +66 -7467
  5. package/dist/graphql-client.js.map +1 -1
  6. package/dist/index.js +0 -61
  7. package/dist/src/client.d.ts +24 -11
  8. package/dist/src/client.d.ts.map +1 -1
  9. package/dist/src/index.d.ts +2 -18
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/types/index.d.ts +7 -38
  12. package/dist/src/types/index.d.ts.map +1 -1
  13. package/dist/types/index.js +1 -1
  14. package/package.json +3 -5
  15. package/src/client.ts +42 -123
  16. package/src/index.ts +3 -70
  17. package/src/types/index.ts +6 -39
  18. package/dist/gql/schema.js +0 -53
  19. package/dist/graphql-client.umd.cjs +0 -199
  20. package/dist/graphql-client.umd.cjs.map +0 -1
  21. package/dist/queries.js +0 -19
  22. package/dist/query.js +0 -231
  23. package/dist/src/client.js +0 -221
  24. package/dist/src/gql/schema.d.ts +0 -7
  25. package/dist/src/gql/schema.d.ts.map +0 -1
  26. package/dist/src/gql/schema.js +0 -53
  27. package/dist/src/index.js +0 -61
  28. package/dist/src/queries.d.ts +0 -9
  29. package/dist/src/queries.d.ts.map +0 -1
  30. package/dist/src/queries.js +0 -19
  31. package/dist/src/query.d.ts +0 -35
  32. package/dist/src/query.d.ts.map +0 -1
  33. package/dist/src/stonecrop-client.d.ts +0 -87
  34. package/dist/src/stonecrop-client.d.ts.map +0 -1
  35. package/dist/src/tsdoc-metadata.json +0 -11
  36. package/dist/src/types/index.js +0 -4
  37. package/dist/stonecrop-client.js +0 -191
  38. package/src/gql/index.d.ts +0 -6
  39. package/src/gql/schema.gql +0 -45
  40. package/src/gql/schema.ts +0 -55
  41. package/src/queries.ts +0 -21
  42. package/src/query.ts +0 -303
package/dist/index.js CHANGED
@@ -1,62 +1 @@
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
1
  export { StonecropClient } from './client';
62
- export { buildRecordQuery, buildListQuery } from './query';
@@ -1,5 +1,7 @@
1
1
  import type { DataClient, DoctypeMeta, DoctypeContext, DoctypeRef, GetRecordOptions, GetRecordsOptions } from '@stonecrop/schema';
2
+ import type { GetRecordResult } from './types';
2
3
  export type { DoctypeContext, DoctypeRef };
4
+ export type { GetRecordResult };
3
5
  /**
4
6
  * Options for creating a Stonecrop client
5
7
  * @public
@@ -9,27 +11,31 @@ export interface StonecropClientOptions {
9
11
  endpoint: string;
10
12
  /** Additional HTTP headers to include in requests */
11
13
  headers?: Record<string, string>;
12
- /** Doctype registry for nested query building */
13
- registry?: Map<string, DoctypeMeta>;
14
14
  }
15
15
  /**
16
- * Client for interacting with Stonecrop GraphQL API
16
+ * Client for interacting with Stonecrop GraphQL API.
17
+ *
18
+ * Acts as a transport layer — it passes requests to the middleware and returns
19
+ * merged results. Does not construct queries itself.
20
+ *
17
21
  * @public
18
22
  */
19
23
  export declare class StonecropClient implements DataClient {
20
24
  private endpoint;
21
25
  private headers;
22
26
  private metaCache;
23
- private registry?;
24
27
  constructor(options: StonecropClientOptions);
25
28
  /**
26
- * Execute a GraphQL query
29
+ * Execute a GraphQL query against the configured endpoint.
30
+ *
27
31
  * @param query - GraphQL query string
28
32
  * @param variables - Query variables
33
+ * @throws Error if the GraphQL response contains errors
29
34
  */
30
35
  query<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T>;
31
36
  /**
32
- * Execute a GraphQL mutation
37
+ * Execute a GraphQL mutation. Delegates to query() since both use POST.
38
+ *
33
39
  * @param mutation - GraphQL mutation string
34
40
  * @param variables - Mutation variables
35
41
  */
@@ -46,16 +52,20 @@ export declare class StonecropClient implements DataClient {
46
52
  /**
47
53
  * Get a single record by ID.
48
54
  *
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.
55
+ * Routes through the stonecropRecord resolver which handles nested data
56
+ * fetching based on the includeNested option.
51
57
  *
52
58
  * @param doctype - Doctype reference (name and optional slug)
53
59
  * @param recordId - Record ID to fetch
54
60
  * @param options - Query options (includeNested, maxDepth)
55
61
  */
56
- getRecord(doctype: DoctypeRef, recordId: string, options?: GetRecordOptions): Promise<Record<string, unknown> | null>;
62
+ getRecord(doctype: DoctypeRef, recordId: string, options?: GetRecordOptions): Promise<GetRecordResult>;
57
63
  /**
58
- * Get multiple records with optional filtering and pagination
64
+ * Get multiple records with optional filtering and pagination.
65
+ *
66
+ * Returns flat arrays — the middleware merges connection format (\{ nodes: [...] \})
67
+ * into plain arrays before returning.
68
+ *
59
69
  * @param doctype - Doctype reference (name and optional slug)
60
70
  * @param options - Query options (filters, orderBy, limit, offset)
61
71
  */
@@ -72,7 +82,10 @@ export declare class StonecropClient implements DataClient {
72
82
  error: string | null;
73
83
  }>;
74
84
  /**
75
- * Clear the cached doctype metadata
85
+ * Clear the cached doctype metadata.
86
+ *
87
+ * Call this if the server-side doctype schema has changed and you need
88
+ * to fetch fresh metadata (e.g., after adding a new field).
76
89
  */
77
90
  clearMetaCache(): void;
78
91
  }
@@ -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,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"}
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;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAA;AAC1C,YAAY,EAAE,eAAe,EAAE,CAAA;AAE/B;;;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;IAmBxF;;;;;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;IAuDnE;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAmD1C;;;;;;;;;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,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;;;;;OAKG;IACH,cAAc,IAAI,IAAI;CAGtB"}
@@ -1,20 +1,4 @@
1
- import { queries } from './queries';
2
- import typeDefs from './gql/schema';
3
- import type { MetaResponse } from './types';
4
- /**
5
- * Get meta information for a doctype
6
- * @param doctype - The doctype to get meta information for
7
- * @param url - The URL to send the request to
8
- * @returns The meta information for the doctype
9
- * @public
10
- */
11
- declare const methods: {
12
- getMeta: (doctype: string, url?: string) => Promise<MetaResponse>;
13
- };
14
- export { queries, typeDefs, methods };
15
- export { StonecropClient } from './client';
16
- export { buildRecordQuery, buildListQuery } from './query';
17
- export type { StonecropClientOptions, DoctypeContext } from './client';
18
- export type { Meta, MetaParser, MetaResponse } from './types';
19
1
  export type { DoctypeMeta } from '@stonecrop/schema';
2
+ export { StonecropClient, type StonecropClientOptions, type DoctypeContext } from './client';
3
+ export type { GetRecordResult } from './types';
20
4
  //# sourceMappingURL=index.d.ts.map
@@ -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,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"}
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,45 +1,14 @@
1
1
  /**
2
- * @file This file contains all the types that are used in the application.
2
+ * @file Types for the GraphQL client.
3
3
  * @public
4
4
  */
5
+ import type { GetRecordResult as SchemaGetRecordResult } from '@stonecrop/schema';
5
6
  /**
6
- * The type of the response from the `getMeta` query.
7
+ * Result from getRecord - includes the record data and any unknown links requested
7
8
  * @public
8
9
  */
9
- export type Meta = {
10
- variables: {
11
- doctype: string;
12
- };
13
- response: {
14
- getMeta: MetaResponse;
15
- };
16
- };
17
- /**
18
- * The type of the response from the `getRecords` query.
19
- * @public
20
- */
21
- export type MetaResponse = {
22
- id: string;
23
- name: string;
24
- workflow: {
25
- id: string;
26
- name: string;
27
- machineId?: string;
28
- };
29
- schema: {
30
- id: string;
31
- label: string;
32
- }[];
33
- actions: {
34
- id: string;
35
- eventName: string;
36
- }[];
37
- };
38
- /**
39
- * The type of the response from the `getMeta` query.
40
- * @public
41
- */
42
- export type MetaParser = {
43
- data: Meta['response'];
44
- };
10
+ export interface GetRecordResult extends SchemaGetRecordResult {
11
+ /** Link names that were requested but don't exist in the doctype schema */
12
+ unknownLinks?: string[];
13
+ }
45
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG;IAClB,SAAS,EAAE;QACV,OAAO,EAAE,MAAM,CAAA;KACf,CAAA;IAED,QAAQ,EAAE;QACT,OAAO,EAAE,YAAY,CAAA;KACrB,CAAA;CACD,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE;QACT,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,SAAS,CAAC,EAAE,MAAM,CAAA;KAClB,CAAA;IACD,MAAM,EAAE;QACP,EAAE,EAAE,MAAM,CAAA;QACV,KAAK,EAAE,MAAM,CAAA;KACb,EAAE,CAAA;IACH,OAAO,EAAE;QACR,EAAE,EAAE,MAAM,CAAA;QACV,SAAS,EAAE,MAAM,CAAA;KACjB,EAAE,CAAA;CACH,CAAA;AAED;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAA;CACtB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,IAAI,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAEjF;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,qBAAqB;IAC7D,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACvB"}
@@ -1,4 +1,4 @@
1
1
  /**
2
- * @file This file contains all the types that are used in the application.
2
+ * @file Types for the GraphQL client.
3
3
  * @public
4
4
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/graphql-client",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "author": {
@@ -30,12 +30,10 @@
30
30
  ],
31
31
  "sideEffects": false,
32
32
  "dependencies": {
33
- "decimal.js": "^10.6.0",
34
33
  "graphql": "^16.12.0",
35
- "graphql-request": "^7.4.0",
36
34
  "pluralize": "^8.0.0",
37
- "@stonecrop/schema": "0.11.0",
38
- "@stonecrop/stonecrop": "0.11.0"
35
+ "@stonecrop/schema": "0.11.2",
36
+ "@stonecrop/stonecrop": "0.11.2"
39
37
  },
40
38
  "devDependencies": {
41
39
  "@eslint/js": "^9.39.2",
package/src/client.ts CHANGED
@@ -6,25 +6,10 @@ import type {
6
6
  GetRecordOptions,
7
7
  GetRecordsOptions,
8
8
  } from '@stonecrop/schema'
9
- import { snakeToCamel, toPascalCase } from '@stonecrop/schema'
10
- import pluralize from 'pluralize'
11
-
12
- import { buildRecordQuery } from './query'
9
+ import type { GetRecordResult } from './types'
13
10
 
14
11
  export type { DoctypeContext, DoctypeRef }
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!'
12
+ export type { GetRecordResult }
28
13
 
29
14
  /**
30
15
  * Options for creating a Stonecrop client
@@ -35,19 +20,20 @@ export interface StonecropClientOptions {
35
20
  endpoint: string
36
21
  /** Additional HTTP headers to include in requests */
37
22
  headers?: Record<string, string>
38
- /** Doctype registry for nested query building */
39
- registry?: Map<string, DoctypeMeta>
40
23
  }
41
24
 
42
25
  /**
43
- * Client for interacting with Stonecrop GraphQL API
26
+ * Client for interacting with Stonecrop GraphQL API.
27
+ *
28
+ * Acts as a transport layer — it passes requests to the middleware and returns
29
+ * merged results. Does not construct queries itself.
30
+ *
44
31
  * @public
45
32
  */
46
33
  export class StonecropClient implements DataClient {
47
34
  private endpoint: string
48
35
  private headers: Record<string, string>
49
36
  private metaCache: Map<string, DoctypeMeta> = new Map()
50
- private registry?: Map<string, DoctypeMeta>
51
37
 
52
38
  constructor(options: StonecropClientOptions) {
53
39
  this.endpoint = options.endpoint
@@ -55,13 +41,14 @@ export class StonecropClient implements DataClient {
55
41
  'Content-Type': 'application/json',
56
42
  ...options.headers,
57
43
  }
58
- this.registry = options.registry
59
44
  }
60
45
 
61
46
  /**
62
- * Execute a GraphQL query
47
+ * Execute a GraphQL query against the configured endpoint.
48
+ *
63
49
  * @param query - GraphQL query string
64
50
  * @param variables - Query variables
51
+ * @throws Error if the GraphQL response contains errors
65
52
  */
66
53
  async query<T = unknown>(query: string, variables?: Record<string, unknown>): Promise<T> {
67
54
  const response = await fetch(this.endpoint, {
@@ -83,7 +70,8 @@ export class StonecropClient implements DataClient {
83
70
  }
84
71
 
85
72
  /**
86
- * Execute a GraphQL mutation
73
+ * Execute a GraphQL mutation. Delegates to query() since both use POST.
74
+ *
87
75
  * @param mutation - GraphQL mutation string
88
76
  * @param variables - Mutation variables
89
77
  */
@@ -207,65 +195,47 @@ export class StonecropClient implements DataClient {
207
195
  /**
208
196
  * Get a single record by ID.
209
197
  *
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.
198
+ * Routes through the stonecropRecord resolver which handles nested data
199
+ * fetching based on the includeNested option.
212
200
  *
213
201
  * @param doctype - Doctype reference (name and optional slug)
214
202
  * @param recordId - Record ID to fetch
215
203
  * @param options - Query options (includeNested, maxDepth)
216
204
  */
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
205
+ async getRecord(doctype: DoctypeRef, recordId: string, options?: GetRecordOptions): Promise<GetRecordResult> {
251
206
  const result = await this.query<{
252
- stonecropRecord: { data: Record<string, unknown> | null }
207
+ stonecropRecord: { data: Record<string, unknown> | null; unknownLinks?: string[] }
253
208
  }>(
254
- `
255
- query GetRecord($doctype: String!, $id: String!) {
256
- stonecropRecord(doctype: $doctype, id: $id) {
209
+ `query GetRecord($doctype: String!, $id: String!, $options: JSON) {
210
+ stonecropRecord(doctype: $doctype, id: $id, options: $options) {
257
211
  data
212
+ unknownLinks
258
213
  }
214
+ }`,
215
+ {
216
+ doctype: doctype.name,
217
+ id: recordId,
218
+ options: options?.includeNested
219
+ ? {
220
+ includeNested: options.includeNested,
221
+ maxDepth: options.maxDepth,
222
+ }
223
+ : undefined,
259
224
  }
260
- `,
261
- { doctype: doctype.name, id: recordId }
262
225
  )
263
226
 
264
- return result.stonecropRecord.data
227
+ return {
228
+ record: result.stonecropRecord?.data ?? null,
229
+ unknownLinks: result.stonecropRecord?.unknownLinks,
230
+ }
265
231
  }
266
232
 
267
233
  /**
268
- * Get multiple records with optional filtering and pagination
234
+ * Get multiple records with optional filtering and pagination.
235
+ *
236
+ * Returns flat arrays — the middleware merges connection format (\{ nodes: [...] \})
237
+ * into plain arrays before returning.
238
+ *
269
239
  * @param doctype - Doctype reference (name and optional slug)
270
240
  * @param options - Query options (filters, orderBy, limit, offset)
271
241
  */
@@ -336,63 +306,12 @@ export class StonecropClient implements DataClient {
336
306
  }
337
307
 
338
308
  /**
339
- * Clear the cached doctype metadata
309
+ * Clear the cached doctype metadata.
310
+ *
311
+ * Call this if the server-side doctype schema has changed and you need
312
+ * to fetch fresh metadata (e.g., after adding a new field).
340
313
  */
341
314
  clearMetaCache(): void {
342
315
  this.metaCache.clear()
343
316
  }
344
317
  }
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
@@ -1,71 +1,4 @@
1
- import { Decimal } from 'decimal.js'
2
- import { GraphQLClient } from 'graphql-request'
3
-
4
- import { queries } from './queries'
5
- import typeDefs from './gql/schema'
6
- import type { Meta, MetaParser, MetaResponse } from './types'
7
-
8
- /**
9
- * Parse the response from the GraphQL server. Converts the stringified JSON to JSON and converts the stringified numbers to Decimal.
10
- * @param obj - The response from the GraphQL server
11
- * @returns The parsed response
12
- * @example
13
- * 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\"}]"}}}'
14
- * const parsedResponse = metaParser(response)
15
- * console.log(parsedResponse)
16
- * /* Output: {"id": "Issue", "name": "Issue", "workflow": { "machineId": null, "name": "save", "id": "1" }, "schema": [{ "label": "Subject", "id": "1" }], "actions": [{ "eventName": "save", "id": "1" }]}
17
- */
18
- const metaParser = (obj: string): MetaParser => {
19
- return JSON.parse(obj, (key, value) => {
20
- if (typeof value === 'string') {
21
- try {
22
- return JSON.parse(value, (_key, value) => {
23
- if (typeof value === 'string' && !isNaN(Number(value))) {
24
- return new Decimal(value)
25
- }
26
-
27
- return value
28
- })
29
- } catch {
30
- // if the value is not a stringified JSON, return as it is
31
- return value
32
- }
33
- } else if (!isNaN(Number(value))) {
34
- return new Decimal(value as string | number)
35
- }
36
- return value
37
- })
38
- }
39
-
40
- /**
41
- * Get meta information for a doctype
42
- * @param doctype - The doctype to get meta information for
43
- * @param url - The URL to send the request to
44
- * @returns The meta information for the doctype
45
- * @public
46
- */
47
- const methods = {
48
- getMeta: async (doctype: string, url?: string): Promise<MetaResponse> => {
49
- const client = new GraphQLClient(url || '/graphql', {
50
- fetch: window.fetch,
51
- jsonSerializer: {
52
- stringify: obj => JSON.stringify(obj), // process the request object before sending; leave as default JSON
53
- parse: metaParser, // process the response meta object
54
- },
55
- })
56
-
57
- const { getMeta } = await client.request<Meta['response'], Meta['variables']>({
58
- document: queries.getMeta,
59
- variables: { doctype },
60
- })
61
-
62
- return getMeta
63
- },
64
- }
65
-
66
- export { queries, typeDefs, methods }
67
- export { StonecropClient } from './client'
68
- export { buildRecordQuery, buildListQuery } from './query'
69
- export type { StonecropClientOptions, DoctypeContext } from './client'
70
- export type { Meta, MetaParser, MetaResponse } from './types'
71
1
  export type { DoctypeMeta } from '@stonecrop/schema'
2
+
3
+ export { StonecropClient, type StonecropClientOptions, type DoctypeContext } from './client'
4
+ export type { GetRecordResult } from './types'
@@ -1,48 +1,15 @@
1
1
  /**
2
- * @file This file contains all the types that are used in the application.
2
+ * @file Types for the GraphQL client.
3
3
  * @public
4
4
  */
5
5
 
6
- /**
7
- * The type of the response from the `getMeta` query.
8
- * @public
9
- */
10
- export type Meta = {
11
- variables: {
12
- doctype: string
13
- }
14
-
15
- response: {
16
- getMeta: MetaResponse
17
- }
18
- }
19
-
20
- /**
21
- * The type of the response from the `getRecords` query.
22
- * @public
23
- */
24
- export type MetaResponse = {
25
- id: string
26
- name: string
27
- workflow: {
28
- id: string
29
- name: string
30
- machineId?: string
31
- }
32
- schema: {
33
- id: string
34
- label: string
35
- }[]
36
- actions: {
37
- id: string
38
- eventName: string
39
- }[]
40
- }
6
+ import type { GetRecordResult as SchemaGetRecordResult } from '@stonecrop/schema'
41
7
 
42
8
  /**
43
- * The type of the response from the `getMeta` query.
9
+ * Result from getRecord - includes the record data and any unknown links requested
44
10
  * @public
45
11
  */
46
- export type MetaParser = {
47
- data: Meta['response']
12
+ export interface GetRecordResult extends SchemaGetRecordResult {
13
+ /** Link names that were requested but don't exist in the doctype schema */
14
+ unknownLinks?: string[]
48
15
  }