@microsoft/rayfin-data 1.20.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/client/DataApi.d.ts +64 -0
- package/dist/client/DataApi.js +109 -0
- package/dist/graphql/GraphQLClient.d.ts +45 -0
- package/dist/graphql/GraphQLClient.js +45 -0
- package/dist/graphql/GraphQLEntityClient.d.ts +48 -0
- package/dist/graphql/GraphQLEntityClient.js +277 -0
- package/dist/graphql/GraphQLQueryBuilder.d.ts +45 -0
- package/dist/graphql/GraphQLQueryBuilder.js +325 -0
- package/dist/graphql/ResponseHandler.d.ts +18 -0
- package/dist/graphql/ResponseHandler.js +66 -0
- package/dist/graphql/types.d.ts +211 -0
- package/dist/graphql/types.js +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/utils/serialization.d.ts +33 -0
- package/dist/utils/serialization.js +132 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) Microsoft Corporation.
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @microsoft/rayfin-data
|
|
2
|
+
|
|
3
|
+
## Security
|
|
4
|
+
|
|
5
|
+
Microsoft takes the security of our software products and services seriously, which
|
|
6
|
+
includes all source code repositories in our GitHub organizations.
|
|
7
|
+
|
|
8
|
+
**Please do not report security vulnerabilities through public GitHub issues.**
|
|
9
|
+
|
|
10
|
+
For security reporting information, locations, contact information, and policies,
|
|
11
|
+
please review the latest guidance for Microsoft repositories at
|
|
12
|
+
[https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md).
|
|
13
|
+
|
|
14
|
+
## Trademarks
|
|
15
|
+
|
|
16
|
+
This project may contain trademarks or logos for projects, products, or services.
|
|
17
|
+
Authorized use of Microsoft trademarks or logos must follow the [Microsoft Trademark and Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general).
|
|
18
|
+
Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship.
|
|
19
|
+
Any use of third-party trademarks or logos is subject to those third parties' policies.
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
Copyright (c) Microsoft Corporation.
|
|
24
|
+
|
|
25
|
+
MIT License
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ApiClient as ApiClientType } from '@microsoft/rayfin-lib';
|
|
2
|
+
import { GraphQLEntityClient } from '../graphql/GraphQLEntityClient';
|
|
3
|
+
import type { EntitySchema } from '../graphql/types';
|
|
4
|
+
/**
|
|
5
|
+
* Type-safe data clients based on entity schema.
|
|
6
|
+
*/
|
|
7
|
+
export type TypedDataClients<TSchema extends EntitySchema> = {
|
|
8
|
+
[K in keyof TSchema]: GraphQLEntityClient<TSchema, K>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Data-specific operations client for interacting with Data API Builder GraphQL endpoints.
|
|
12
|
+
*
|
|
13
|
+
* Use the `createDataApi` factory function to create instances instead of instantiating directly.
|
|
14
|
+
*
|
|
15
|
+
* @template TSchema - Object type mapping entity names to their types
|
|
16
|
+
*/
|
|
17
|
+
export declare class DataApi<TSchema extends EntitySchema = Record<string, any>> {
|
|
18
|
+
private apiClient;
|
|
19
|
+
private clientCache;
|
|
20
|
+
constructor(apiClient: ApiClientType);
|
|
21
|
+
/**
|
|
22
|
+
* Get a GraphQL client for a specific entity.
|
|
23
|
+
*/
|
|
24
|
+
getClient<K extends keyof TSchema>(entityName: K): GraphQLEntityClient<TSchema, K>;
|
|
25
|
+
/**
|
|
26
|
+
* Get all available entity names.
|
|
27
|
+
* In a Proxy-based client, this returns all entity names that have been accessed.
|
|
28
|
+
*/
|
|
29
|
+
getEntityNames(): (keyof TSchema)[];
|
|
30
|
+
/**
|
|
31
|
+
* Create a Proxy that generates GraphQL clients on-demand.
|
|
32
|
+
*/
|
|
33
|
+
private createProxy;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a type-safe DataApi instance for interacting with Data API Builder GraphQL endpoints.
|
|
37
|
+
*
|
|
38
|
+
* @template TSchema - Object type mapping entity names to their types
|
|
39
|
+
* @param apiClient - The ApiClient instance for making HTTP requests
|
|
40
|
+
* @returns A fully typed DataApi instance with direct entity access
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* const dataApi = createDataApi<{
|
|
45
|
+
* User: User;
|
|
46
|
+
* Organization: Organization;
|
|
47
|
+
* }>(apiClient);
|
|
48
|
+
*
|
|
49
|
+
* // GraphQL fluent interface (direct entity access):
|
|
50
|
+
* const activeUsers = await dataApi.User
|
|
51
|
+
* .select(['id', 'name', 'email'])
|
|
52
|
+
* .where({ isActive: { eq: true } })
|
|
53
|
+
* .orderBy({ createdAt: 'desc' })
|
|
54
|
+
* .execute();
|
|
55
|
+
*
|
|
56
|
+
* // GraphQL mutations:
|
|
57
|
+
* const newUser = await dataApi.User.create({
|
|
58
|
+
* email: 'user@example.com',
|
|
59
|
+
* name: 'Jane Doe'
|
|
60
|
+
* });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDataApi<TSchema extends EntitySchema>(apiClient: ApiClientType): DataApi<TSchema> & TypedDataClients<TSchema>;
|
|
64
|
+
//# sourceMappingURL=DataApi.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { GraphQLClient } from '../graphql/GraphQLClient';
|
|
2
|
+
import { GraphQLEntityClient } from '../graphql/GraphQLEntityClient';
|
|
3
|
+
/**
|
|
4
|
+
* Data-specific operations client for interacting with Data API Builder GraphQL endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Use the `createDataApi` factory function to create instances instead of instantiating directly.
|
|
7
|
+
*
|
|
8
|
+
* @template TSchema - Object type mapping entity names to their types
|
|
9
|
+
*/
|
|
10
|
+
export class DataApi {
|
|
11
|
+
apiClient;
|
|
12
|
+
clientCache = new Map();
|
|
13
|
+
// Single constructor - requires ApiClient injection
|
|
14
|
+
constructor(apiClient) {
|
|
15
|
+
this.apiClient = apiClient;
|
|
16
|
+
// Return a Proxy that generates GraphQL clients on-demand
|
|
17
|
+
return this.createProxy();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get a GraphQL client for a specific entity.
|
|
21
|
+
*/
|
|
22
|
+
getClient(entityName) {
|
|
23
|
+
const name = String(entityName);
|
|
24
|
+
if (!this.clientCache.has(name)) {
|
|
25
|
+
const graphqlClient = new GraphQLClient(this.apiClient, `/graphql`);
|
|
26
|
+
this.clientCache.set(name, new GraphQLEntityClient(graphqlClient, entityName));
|
|
27
|
+
}
|
|
28
|
+
return this.clientCache.get(name);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Get all available entity names.
|
|
32
|
+
* In a Proxy-based client, this returns all entity names that have been accessed.
|
|
33
|
+
*/
|
|
34
|
+
getEntityNames() {
|
|
35
|
+
return Array.from(this.clientCache.keys());
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create a Proxy that generates GraphQL clients on-demand.
|
|
39
|
+
*/
|
|
40
|
+
createProxy() {
|
|
41
|
+
return new Proxy(this, {
|
|
42
|
+
get: (target, prop) => {
|
|
43
|
+
// Allow access to class methods and properties
|
|
44
|
+
if (prop in target) {
|
|
45
|
+
return target[prop];
|
|
46
|
+
}
|
|
47
|
+
// Generate entity client for string properties
|
|
48
|
+
if (typeof prop === 'string') {
|
|
49
|
+
return this.getClient(prop);
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
},
|
|
53
|
+
ownKeys: () => {
|
|
54
|
+
// Return only entity names for Object.keys() etc.
|
|
55
|
+
return Array.from(this.clientCache.keys());
|
|
56
|
+
},
|
|
57
|
+
has: (target, prop) => {
|
|
58
|
+
// Check class properties first
|
|
59
|
+
if (prop in target) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
// Always return true for string properties (entity names)
|
|
63
|
+
return typeof prop === 'string';
|
|
64
|
+
},
|
|
65
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
66
|
+
if (typeof prop === 'string') {
|
|
67
|
+
return {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
value: this.getClient(prop),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a type-safe DataApi instance for interacting with Data API Builder GraphQL endpoints.
|
|
80
|
+
*
|
|
81
|
+
* @template TSchema - Object type mapping entity names to their types
|
|
82
|
+
* @param apiClient - The ApiClient instance for making HTTP requests
|
|
83
|
+
* @returns A fully typed DataApi instance with direct entity access
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const dataApi = createDataApi<{
|
|
88
|
+
* User: User;
|
|
89
|
+
* Organization: Organization;
|
|
90
|
+
* }>(apiClient);
|
|
91
|
+
*
|
|
92
|
+
* // GraphQL fluent interface (direct entity access):
|
|
93
|
+
* const activeUsers = await dataApi.User
|
|
94
|
+
* .select(['id', 'name', 'email'])
|
|
95
|
+
* .where({ isActive: { eq: true } })
|
|
96
|
+
* .orderBy({ createdAt: 'desc' })
|
|
97
|
+
* .execute();
|
|
98
|
+
*
|
|
99
|
+
* // GraphQL mutations:
|
|
100
|
+
* const newUser = await dataApi.User.create({
|
|
101
|
+
* email: 'user@example.com',
|
|
102
|
+
* name: 'Jane Doe'
|
|
103
|
+
* });
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function createDataApi(apiClient) {
|
|
107
|
+
return new DataApi(apiClient);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=DataApi.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ApiClient } from '@microsoft/rayfin-lib';
|
|
2
|
+
/**
|
|
3
|
+
* GraphQL request payload.
|
|
4
|
+
*/
|
|
5
|
+
export interface GraphQLRequest {
|
|
6
|
+
query: string;
|
|
7
|
+
variables?: Record<string, any>;
|
|
8
|
+
operationName?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* GraphQL response payload.
|
|
12
|
+
*/
|
|
13
|
+
export interface GraphQLResponse<T = any> {
|
|
14
|
+
data?: T;
|
|
15
|
+
errors?: Array<{
|
|
16
|
+
message: string;
|
|
17
|
+
locations?: Array<{
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
}>;
|
|
21
|
+
path?: Array<string | number>;
|
|
22
|
+
extensions?: Record<string, any>;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* GraphQL client for executing queries and mutations.
|
|
27
|
+
*/
|
|
28
|
+
export declare class GraphQLClient {
|
|
29
|
+
private apiClient;
|
|
30
|
+
private endpoint;
|
|
31
|
+
constructor(apiClient: ApiClient, endpoint?: string);
|
|
32
|
+
/**
|
|
33
|
+
* Execute a GraphQL query or mutation.
|
|
34
|
+
*/
|
|
35
|
+
request<T = any>(query: string, variables?: Record<string, any>, operationName?: string): Promise<T>;
|
|
36
|
+
/**
|
|
37
|
+
* Execute a GraphQL query.
|
|
38
|
+
*/
|
|
39
|
+
query<T = any>(query: string, variables?: Record<string, any>): Promise<T>;
|
|
40
|
+
/**
|
|
41
|
+
* Execute a GraphQL mutation.
|
|
42
|
+
*/
|
|
43
|
+
mutation<T = any>(mutation: string, variables?: Record<string, any>): Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=GraphQLClient.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL client for executing queries and mutations.
|
|
3
|
+
*/
|
|
4
|
+
export class GraphQLClient {
|
|
5
|
+
apiClient;
|
|
6
|
+
endpoint;
|
|
7
|
+
constructor(apiClient, endpoint = '/graphql') {
|
|
8
|
+
this.apiClient = apiClient;
|
|
9
|
+
this.endpoint = endpoint;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Execute a GraphQL query or mutation.
|
|
13
|
+
*/
|
|
14
|
+
async request(query, variables, operationName) {
|
|
15
|
+
const payload = {
|
|
16
|
+
query,
|
|
17
|
+
variables,
|
|
18
|
+
operationName,
|
|
19
|
+
};
|
|
20
|
+
const response = await this.apiClient.post(this.endpoint, payload);
|
|
21
|
+
if (response.errors && response.errors.length > 0) {
|
|
22
|
+
const errorMessage = response.errors
|
|
23
|
+
.map((err) => err.message)
|
|
24
|
+
.join(', ');
|
|
25
|
+
throw new Error(`GraphQL errors: ${errorMessage}`);
|
|
26
|
+
}
|
|
27
|
+
if (response.data === undefined) {
|
|
28
|
+
throw new Error('GraphQL response contains no data');
|
|
29
|
+
}
|
|
30
|
+
return response.data;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Execute a GraphQL query.
|
|
34
|
+
*/
|
|
35
|
+
async query(query, variables) {
|
|
36
|
+
return this.request(query, variables);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute a GraphQL mutation.
|
|
40
|
+
*/
|
|
41
|
+
async mutation(mutation, variables) {
|
|
42
|
+
return this.request(mutation, variables);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=GraphQLClient.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { GraphQLClient } from './GraphQLClient';
|
|
2
|
+
import { GraphQLQueryBuilder } from './GraphQLQueryBuilder';
|
|
3
|
+
import type { EntitySchema, CreateInput, UpdateInput, WhereUniqueInput, FilterInput, OrderByInput, FieldSelection } from './types';
|
|
4
|
+
export declare class GraphQLEntityClient<TSchema extends EntitySchema, TEntity extends keyof TSchema> {
|
|
5
|
+
private graphqlClient;
|
|
6
|
+
private entityNameString;
|
|
7
|
+
constructor(graphqlClient: GraphQLClient, entityName: TEntity);
|
|
8
|
+
private lowercaseFirstLetter;
|
|
9
|
+
select<TFields extends FieldSelection<TSchema[TEntity]>>(fields: TFields): GraphQLQueryBuilder<TSchema, TEntity>;
|
|
10
|
+
where(conditions: FilterInput<TSchema[TEntity]>): GraphQLQueryBuilder<TSchema, TEntity>;
|
|
11
|
+
orderBy(order: OrderByInput<TSchema[TEntity]>): GraphQLQueryBuilder<TSchema, TEntity>;
|
|
12
|
+
first(count: number): GraphQLQueryBuilder<TSchema, TEntity>;
|
|
13
|
+
findMany(filter?: FilterInput<TSchema[TEntity]>): Promise<TSchema[TEntity][]>;
|
|
14
|
+
findFirst(filter?: FilterInput<TSchema[TEntity]>): Promise<TSchema[TEntity] | null>;
|
|
15
|
+
findById(id: string): Promise<TSchema[TEntity] | null>;
|
|
16
|
+
create(input: CreateInput<TSchema[TEntity]>): Promise<TSchema[TEntity]>;
|
|
17
|
+
update(where: WhereUniqueInput<TSchema[TEntity]>, data: UpdateInput<TSchema[TEntity]>): Promise<TSchema[TEntity]>;
|
|
18
|
+
delete(where: WhereUniqueInput<TSchema[TEntity]>): Promise<TSchema[TEntity]>;
|
|
19
|
+
upsert(where: WhereUniqueInput<TSchema[TEntity]>, create: CreateInput<TSchema[TEntity]>, update: UpdateInput<TSchema[TEntity]>): Promise<TSchema[TEntity]>;
|
|
20
|
+
protected buildCreateMutationString(input: CreateInput<TSchema[TEntity]>): string;
|
|
21
|
+
protected buildUpdateMutationString(where: WhereUniqueInput<TSchema[TEntity]>, data: UpdateInput<TSchema[TEntity]>): string;
|
|
22
|
+
protected buildDeleteMutationString(where: WhereUniqueInput<TSchema[TEntity]>): string;
|
|
23
|
+
protected getDefaultFields(data: any): string;
|
|
24
|
+
protected formatMutationInput(data: any): string;
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a value represents a relationship object (has 'id' property)
|
|
27
|
+
*/
|
|
28
|
+
private isRelationshipObject;
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the ID value from a relationship object, handling both 'id' and 'Id' casing
|
|
31
|
+
*/
|
|
32
|
+
private getRelationshipId;
|
|
33
|
+
/**
|
|
34
|
+
* Checks if a value is a primitive field (not a complex object)
|
|
35
|
+
*/
|
|
36
|
+
private isPrimitiveField;
|
|
37
|
+
private formatValue;
|
|
38
|
+
protected extractId(where: WhereUniqueInput<TSchema[TEntity]>): string;
|
|
39
|
+
private extractMutationResult;
|
|
40
|
+
/**
|
|
41
|
+
* Transforms flat FK fields (e.g. category_id) in mutation responses back into
|
|
42
|
+
* nested relationship objects (e.g. category: { id: "..." }) based on the
|
|
43
|
+
* relationship keys present in the original input.
|
|
44
|
+
*/
|
|
45
|
+
private nestRelationshipFields;
|
|
46
|
+
private isNotFoundError;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=GraphQLEntityClient.d.ts.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { getPrimaryKeyField } from '@microsoft/rayfin-core';
|
|
2
|
+
import { deserializeDabResponse } from '../utils/serialization';
|
|
3
|
+
import { GraphQLQueryBuilder } from './GraphQLQueryBuilder';
|
|
4
|
+
export class GraphQLEntityClient {
|
|
5
|
+
graphqlClient;
|
|
6
|
+
entityNameString;
|
|
7
|
+
constructor(graphqlClient, entityName) {
|
|
8
|
+
this.graphqlClient = graphqlClient;
|
|
9
|
+
this.entityNameString = String(entityName);
|
|
10
|
+
}
|
|
11
|
+
// Helper method to lowercase only the first letter of the entity name
|
|
12
|
+
lowercaseFirstLetter(str) {
|
|
13
|
+
if (!str)
|
|
14
|
+
return str;
|
|
15
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
16
|
+
}
|
|
17
|
+
// === QUERY METHODS (return builders for fluent interface) ===
|
|
18
|
+
select(fields) {
|
|
19
|
+
return new GraphQLQueryBuilder(this.graphqlClient, this.entityNameString).select(fields);
|
|
20
|
+
}
|
|
21
|
+
where(conditions) {
|
|
22
|
+
return new GraphQLQueryBuilder(this.graphqlClient, this.entityNameString).where(conditions);
|
|
23
|
+
}
|
|
24
|
+
orderBy(order) {
|
|
25
|
+
return new GraphQLQueryBuilder(this.graphqlClient, this.entityNameString).orderBy(order);
|
|
26
|
+
}
|
|
27
|
+
first(count) {
|
|
28
|
+
return new GraphQLQueryBuilder(this.graphqlClient, this.entityNameString).first(count);
|
|
29
|
+
}
|
|
30
|
+
// === DIRECT QUERY METHODS ===
|
|
31
|
+
async findMany(filter) {
|
|
32
|
+
const builder = new GraphQLQueryBuilder(this.graphqlClient, this.entityNameString);
|
|
33
|
+
if (filter) {
|
|
34
|
+
builder.where(filter);
|
|
35
|
+
}
|
|
36
|
+
return builder.execute();
|
|
37
|
+
}
|
|
38
|
+
async findFirst(filter) {
|
|
39
|
+
const builder = new GraphQLQueryBuilder(this.graphqlClient, this.entityNameString).first(1);
|
|
40
|
+
if (filter) {
|
|
41
|
+
builder.where(filter);
|
|
42
|
+
}
|
|
43
|
+
const results = await builder.execute();
|
|
44
|
+
return results[0] || null;
|
|
45
|
+
}
|
|
46
|
+
async findById(id) {
|
|
47
|
+
const queryName = `${this.lowercaseFirstLetter(this.entityNameString)}_by_pk`;
|
|
48
|
+
const query = `
|
|
49
|
+
query {
|
|
50
|
+
${queryName}(${getPrimaryKeyField()}: "${id}") {
|
|
51
|
+
${getPrimaryKeyField()}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
try {
|
|
56
|
+
const result = await this.graphqlClient.query(query);
|
|
57
|
+
const entityData = result.data?.[queryName] || result[queryName] || null;
|
|
58
|
+
return entityData
|
|
59
|
+
? deserializeDabResponse(entityData)
|
|
60
|
+
: null;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (this.isNotFoundError(error)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// === MUTATION METHODS ===
|
|
70
|
+
async create(input) {
|
|
71
|
+
const mutationName = `create${this.entityNameString}`;
|
|
72
|
+
const mutation = this.buildCreateMutationString(input);
|
|
73
|
+
const result = await this.graphqlClient.mutation(mutation);
|
|
74
|
+
const entityData = this.extractMutationResult(result, mutationName);
|
|
75
|
+
const deserialized = deserializeDabResponse(entityData);
|
|
76
|
+
return this.nestRelationshipFields(deserialized, input);
|
|
77
|
+
}
|
|
78
|
+
async update(where, data) {
|
|
79
|
+
const mutationName = `update${this.entityNameString}`;
|
|
80
|
+
const mutation = this.buildUpdateMutationString(where, data);
|
|
81
|
+
const result = await this.graphqlClient.mutation(mutation);
|
|
82
|
+
const entityData = this.extractMutationResult(result, mutationName);
|
|
83
|
+
const deserialized = deserializeDabResponse(entityData);
|
|
84
|
+
return this.nestRelationshipFields(deserialized, data);
|
|
85
|
+
}
|
|
86
|
+
async delete(where) {
|
|
87
|
+
const mutationName = `delete${this.entityNameString}`;
|
|
88
|
+
const mutation = this.buildDeleteMutationString(where);
|
|
89
|
+
const result = await this.graphqlClient.mutation(mutation);
|
|
90
|
+
const entityData = this.extractMutationResult(result, mutationName);
|
|
91
|
+
return deserializeDabResponse(entityData);
|
|
92
|
+
}
|
|
93
|
+
async upsert(where, create, update) {
|
|
94
|
+
const id = this.extractId(where);
|
|
95
|
+
const existing = await this.findById(id);
|
|
96
|
+
if (existing) {
|
|
97
|
+
return await this.update(where, update);
|
|
98
|
+
}
|
|
99
|
+
return await this.create(create);
|
|
100
|
+
}
|
|
101
|
+
// === PROTECTED MUTATION BUILDING METHODS (for testing access) ===
|
|
102
|
+
buildCreateMutationString(input) {
|
|
103
|
+
const mutationName = `create${this.entityNameString}`;
|
|
104
|
+
const fieldsToReturn = this.getDefaultFields(input);
|
|
105
|
+
return `
|
|
106
|
+
mutation {
|
|
107
|
+
${mutationName}(item: ${this.formatMutationInput(input)}) {
|
|
108
|
+
${fieldsToReturn}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
`.trim();
|
|
112
|
+
}
|
|
113
|
+
buildUpdateMutationString(where, data) {
|
|
114
|
+
const mutationName = `update${this.entityNameString}`;
|
|
115
|
+
const id = this.extractId(where);
|
|
116
|
+
const fieldsToReturn = this.getDefaultFields(data);
|
|
117
|
+
return `
|
|
118
|
+
mutation {
|
|
119
|
+
${mutationName}(
|
|
120
|
+
${getPrimaryKeyField()}: "${id}",
|
|
121
|
+
item: ${this.formatMutationInput(data)}
|
|
122
|
+
) {
|
|
123
|
+
${fieldsToReturn}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`.trim();
|
|
127
|
+
}
|
|
128
|
+
buildDeleteMutationString(where) {
|
|
129
|
+
const mutationName = `delete${this.entityNameString}`;
|
|
130
|
+
const id = this.extractId(where);
|
|
131
|
+
return `
|
|
132
|
+
mutation {
|
|
133
|
+
${mutationName}(${getPrimaryKeyField()}: "${id}") {
|
|
134
|
+
${getPrimaryKeyField()}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`.trim();
|
|
138
|
+
}
|
|
139
|
+
// === PROTECTED HELPER METHODS (for testing access) ===
|
|
140
|
+
getDefaultFields(data) {
|
|
141
|
+
const fields = [];
|
|
142
|
+
// Process all entries with unified logic
|
|
143
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
144
|
+
// Skip undefined values
|
|
145
|
+
if (value === undefined) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Handle relationship objects by adding foreign key fields
|
|
149
|
+
if (this.isRelationshipObject(value)) {
|
|
150
|
+
const foreignKeyField = `${key}_${getPrimaryKeyField()}`;
|
|
151
|
+
if (!fields.includes(foreignKeyField)) {
|
|
152
|
+
fields.push(foreignKeyField);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Handle primitive fields (exclude other object types)
|
|
156
|
+
else if (this.isPrimitiveField(value)) {
|
|
157
|
+
fields.push(key);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
// Ensure the PK field is always included and at the beginning
|
|
161
|
+
if (!fields.includes(getPrimaryKeyField())) {
|
|
162
|
+
fields.unshift(getPrimaryKeyField());
|
|
163
|
+
}
|
|
164
|
+
return fields.join('\n ');
|
|
165
|
+
}
|
|
166
|
+
formatMutationInput(data) {
|
|
167
|
+
const entries = [];
|
|
168
|
+
// Process all entries with unified logic
|
|
169
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
170
|
+
// Skip undefined values
|
|
171
|
+
if (value === undefined) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Handle relationship objects by converting to foreign key
|
|
175
|
+
if (this.isRelationshipObject(value)) {
|
|
176
|
+
const foreignKeyField = `${key}_${getPrimaryKeyField()}`;
|
|
177
|
+
const foreignKeyValue = this.formatValue(this.getRelationshipId(value));
|
|
178
|
+
entries.push(`${foreignKeyField}: ${foreignKeyValue}`);
|
|
179
|
+
}
|
|
180
|
+
// Handle primitive fields (exclude other object types)
|
|
181
|
+
else if (this.isPrimitiveField(value)) {
|
|
182
|
+
entries.push(`${key}: ${this.formatValue(value)}`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return `{ ${entries.join(', ')} }`;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Checks if a value represents a relationship object (has 'id' property)
|
|
189
|
+
*/
|
|
190
|
+
isRelationshipObject(value) {
|
|
191
|
+
return (typeof value === 'object' &&
|
|
192
|
+
value !== null &&
|
|
193
|
+
!Array.isArray(value) &&
|
|
194
|
+
!(value instanceof Date) &&
|
|
195
|
+
('id' in value || 'Id' in value));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Extracts the ID value from a relationship object, handling both 'id' and 'Id' casing
|
|
199
|
+
*/
|
|
200
|
+
getRelationshipId(value) {
|
|
201
|
+
if ('id' in value) {
|
|
202
|
+
return value.id;
|
|
203
|
+
}
|
|
204
|
+
if ('Id' in value) {
|
|
205
|
+
return value.Id;
|
|
206
|
+
}
|
|
207
|
+
throw new Error('No ID field found in relationship object');
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Checks if a value is a primitive field (not a complex object)
|
|
211
|
+
*/
|
|
212
|
+
isPrimitiveField(value) {
|
|
213
|
+
return !(typeof value === 'object' &&
|
|
214
|
+
value !== null &&
|
|
215
|
+
!Array.isArray(value) &&
|
|
216
|
+
!(value instanceof Date));
|
|
217
|
+
}
|
|
218
|
+
// === PRIVATE HELPER METHODS ===
|
|
219
|
+
formatValue(value) {
|
|
220
|
+
if (typeof value === 'string') {
|
|
221
|
+
return `"${value
|
|
222
|
+
.replace(/\\/g, '\\\\')
|
|
223
|
+
.replace(/"/g, '\\"')
|
|
224
|
+
.replace(/\n/g, '\\n')
|
|
225
|
+
.replace(/\r/g, '\\r')
|
|
226
|
+
.replace(/\t/g, '\\t')}"`;
|
|
227
|
+
}
|
|
228
|
+
if (typeof value === 'boolean') {
|
|
229
|
+
return String(value);
|
|
230
|
+
}
|
|
231
|
+
if (typeof value === 'number') {
|
|
232
|
+
return String(value);
|
|
233
|
+
}
|
|
234
|
+
if (value instanceof Date) {
|
|
235
|
+
return `"${value.toISOString()}"`;
|
|
236
|
+
}
|
|
237
|
+
if (value === null || value === undefined) {
|
|
238
|
+
return 'null';
|
|
239
|
+
}
|
|
240
|
+
return `"${String(value)}"`;
|
|
241
|
+
}
|
|
242
|
+
extractId(where) {
|
|
243
|
+
return where[getPrimaryKeyField()];
|
|
244
|
+
}
|
|
245
|
+
extractMutationResult(result, operationName) {
|
|
246
|
+
if (result?.data?.[operationName]) {
|
|
247
|
+
return result.data[operationName];
|
|
248
|
+
}
|
|
249
|
+
if (result[operationName]) {
|
|
250
|
+
return result[operationName];
|
|
251
|
+
}
|
|
252
|
+
throw new Error(`Failed to extract result from ${operationName} mutation`);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Transforms flat FK fields (e.g. category_id) in mutation responses back into
|
|
256
|
+
* nested relationship objects (e.g. category: { id: "..." }) based on the
|
|
257
|
+
* relationship keys present in the original input.
|
|
258
|
+
*/
|
|
259
|
+
nestRelationshipFields(responseData, inputData) {
|
|
260
|
+
const result = { ...responseData };
|
|
261
|
+
Object.entries(inputData).forEach(([key, value]) => {
|
|
262
|
+
if (value !== undefined && this.isRelationshipObject(value)) {
|
|
263
|
+
const foreignKeyField = `${key}_${getPrimaryKeyField()}`;
|
|
264
|
+
if (foreignKeyField in result) {
|
|
265
|
+
result[key] = { [getPrimaryKeyField()]: result[foreignKeyField] };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
isNotFoundError(error) {
|
|
272
|
+
return (error?.message?.includes('not found') ||
|
|
273
|
+
error?.message?.includes('404') ||
|
|
274
|
+
error?.extensions?.code === 'NOT_FOUND');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
//# sourceMappingURL=GraphQLEntityClient.js.map
|