@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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { GraphQLClient } from './GraphQLClient';
|
|
2
|
+
import type { EntitySchema, FilterInput, OrderByInput, PagedResult, FieldSelection } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Fluent GraphQL query builder for constructing and executing complex queries.
|
|
5
|
+
*
|
|
6
|
+
* @template TSchema The entity schema type
|
|
7
|
+
* @template TEntity The specific entity name
|
|
8
|
+
*/
|
|
9
|
+
export declare class GraphQLQueryBuilder<TSchema extends EntitySchema, TEntity extends keyof TSchema> {
|
|
10
|
+
private graphqlClient;
|
|
11
|
+
private selections;
|
|
12
|
+
private whereConditions;
|
|
13
|
+
private orderByConditions;
|
|
14
|
+
private paginationConfig;
|
|
15
|
+
private readonly entityPluralName;
|
|
16
|
+
constructor(graphqlClient: GraphQLClient, entityName: string);
|
|
17
|
+
private lowercaseFirstLetter;
|
|
18
|
+
select<TFields extends FieldSelection<TSchema[TEntity]>>(fields: TFields): this;
|
|
19
|
+
where(conditions: FilterInput<TSchema[TEntity]>): this;
|
|
20
|
+
orderBy(order: OrderByInput<TSchema[TEntity]>): this;
|
|
21
|
+
first(count: number): this;
|
|
22
|
+
after(cursor: string): this;
|
|
23
|
+
execute(): Promise<TSchema[TEntity][]>;
|
|
24
|
+
executePaginated(): Promise<PagedResult<TSchema[TEntity]>>;
|
|
25
|
+
findFirst(): Promise<TSchema[TEntity] | null>;
|
|
26
|
+
protected buildQuery(): string;
|
|
27
|
+
protected buildQueryWithPagination(): string;
|
|
28
|
+
private buildFieldSelection;
|
|
29
|
+
/**
|
|
30
|
+
* DAB represents collection navigation properties as "Connection" objects containing an `items` array.
|
|
31
|
+
* For ergonomics (and to match our schema types like `todos?: Todo[]`), unwrap any selected nested
|
|
32
|
+
* connection objects to their `items` arrays.
|
|
33
|
+
*/
|
|
34
|
+
private unwrapNestedConnectionItems;
|
|
35
|
+
private buildArguments;
|
|
36
|
+
private buildFilterObject;
|
|
37
|
+
private buildFieldFilter;
|
|
38
|
+
private isRelationshipNullFilter;
|
|
39
|
+
private isNullFilterObject;
|
|
40
|
+
private hasNestedSelectionFor;
|
|
41
|
+
private buildOrderByArray;
|
|
42
|
+
private formatValue;
|
|
43
|
+
private mergeFilters;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=GraphQLQueryBuilder.d.ts.map
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { getPrimaryKeyField } from '@microsoft/rayfin-core';
|
|
2
|
+
import { EntityNameResolver } from '@microsoft/rayfin-lib';
|
|
3
|
+
import { ResponseHandler } from './ResponseHandler';
|
|
4
|
+
/**
|
|
5
|
+
* Fluent GraphQL query builder for constructing and executing complex queries.
|
|
6
|
+
*
|
|
7
|
+
* @template TSchema The entity schema type
|
|
8
|
+
* @template TEntity The specific entity name
|
|
9
|
+
*/
|
|
10
|
+
export class GraphQLQueryBuilder {
|
|
11
|
+
graphqlClient;
|
|
12
|
+
selections = [];
|
|
13
|
+
whereConditions = {};
|
|
14
|
+
orderByConditions = [];
|
|
15
|
+
paginationConfig = {};
|
|
16
|
+
entityPluralName;
|
|
17
|
+
constructor(graphqlClient, entityName) {
|
|
18
|
+
this.graphqlClient = graphqlClient;
|
|
19
|
+
// Use EntityNameResolver for proper pluralization and convert to lowercase for DAB compliance
|
|
20
|
+
const pluralName = EntityNameResolver.getPlural(entityName);
|
|
21
|
+
this.entityPluralName = this.lowercaseFirstLetter(pluralName);
|
|
22
|
+
}
|
|
23
|
+
// Helper method to lowercase only the first letter of the entity name
|
|
24
|
+
lowercaseFirstLetter(str) {
|
|
25
|
+
if (!str)
|
|
26
|
+
return str;
|
|
27
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
28
|
+
}
|
|
29
|
+
// === FLUENT BUILDER METHODS ===
|
|
30
|
+
select(fields) {
|
|
31
|
+
this.selections = [...fields];
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
where(conditions) {
|
|
35
|
+
this.whereConditions = this.mergeFilters(this.whereConditions, conditions);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
orderBy(order) {
|
|
39
|
+
this.orderByConditions.push(order);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
first(count) {
|
|
43
|
+
this.paginationConfig.first = count;
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
after(cursor) {
|
|
47
|
+
this.paginationConfig.after = cursor;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
// === EXECUTION METHODS ===
|
|
51
|
+
async execute() {
|
|
52
|
+
const query = this.buildQuery();
|
|
53
|
+
const result = await this.graphqlClient.query(query);
|
|
54
|
+
const unwrapped = ResponseHandler.unwrapGraphQLResponse(result, this.entityPluralName, false);
|
|
55
|
+
const items = Array.isArray(unwrapped) ? unwrapped : unwrapped.items;
|
|
56
|
+
return this.unwrapNestedConnectionItems(items);
|
|
57
|
+
}
|
|
58
|
+
async executePaginated() {
|
|
59
|
+
const query = this.buildQueryWithPagination();
|
|
60
|
+
const result = await this.graphqlClient.query(query);
|
|
61
|
+
const unwrapped = ResponseHandler.unwrapGraphQLResponse(result, this.entityPluralName, true);
|
|
62
|
+
const paged = unwrapped;
|
|
63
|
+
return {
|
|
64
|
+
...paged,
|
|
65
|
+
items: this.unwrapNestedConnectionItems(paged.items),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async findFirst() {
|
|
69
|
+
const originalFirst = this.paginationConfig.first;
|
|
70
|
+
this.paginationConfig.first = 1;
|
|
71
|
+
try {
|
|
72
|
+
const results = await this.execute();
|
|
73
|
+
return results[0] || null;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.paginationConfig.first = originalFirst;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// === PROTECTED QUERY BUILDING METHODS (for testing access) ===
|
|
80
|
+
buildQuery() {
|
|
81
|
+
const fields = this.buildFieldSelection();
|
|
82
|
+
const args = this.buildArguments();
|
|
83
|
+
return `
|
|
84
|
+
query {
|
|
85
|
+
${this.entityPluralName}${args} {
|
|
86
|
+
items {
|
|
87
|
+
${fields}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
`.trim();
|
|
92
|
+
}
|
|
93
|
+
buildQueryWithPagination() {
|
|
94
|
+
const fields = this.buildFieldSelection();
|
|
95
|
+
const args = this.buildArguments();
|
|
96
|
+
return `
|
|
97
|
+
query {
|
|
98
|
+
${this.entityPluralName}${args} {
|
|
99
|
+
items {
|
|
100
|
+
${fields}
|
|
101
|
+
}
|
|
102
|
+
endCursor
|
|
103
|
+
hasNextPage
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
`.trim();
|
|
107
|
+
}
|
|
108
|
+
buildFieldSelection() {
|
|
109
|
+
if (this.selections.length === 0) {
|
|
110
|
+
return 'id';
|
|
111
|
+
}
|
|
112
|
+
// Group fields by their root and nested parts
|
|
113
|
+
const fieldMap = new Map();
|
|
114
|
+
this.selections.forEach((field) => {
|
|
115
|
+
const fieldStr = String(field);
|
|
116
|
+
if (fieldStr.includes('.')) {
|
|
117
|
+
const [root, nested] = fieldStr.split('.', 2);
|
|
118
|
+
if (!fieldMap.has(root)) {
|
|
119
|
+
fieldMap.set(root, new Set());
|
|
120
|
+
}
|
|
121
|
+
fieldMap.get(root).add(nested);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
fieldMap.set(fieldStr, new Set());
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
// Build GraphQL field selection
|
|
128
|
+
return Array.from(fieldMap.entries())
|
|
129
|
+
.map(([field, nestedFields]) => {
|
|
130
|
+
if (nestedFields.size === 0) {
|
|
131
|
+
return field;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const nestedSelection = Array.from(nestedFields).join('\n ');
|
|
135
|
+
// DAB GraphQL represents collections (e.g. one-to-many) as *Connection objects
|
|
136
|
+
// with an `items` field. Since our selection DSL uses `posts.id` / `todos.Title`
|
|
137
|
+
// for array relationships, we need to shape the GraphQL selection accordingly.
|
|
138
|
+
//
|
|
139
|
+
// Use EntityNameResolver to properly detect plural fields instead of crude string suffix check
|
|
140
|
+
if (EntityNameResolver.isPlural(field)) {
|
|
141
|
+
return `${field} {\n items {\n ${nestedSelection}\n }\n }`;
|
|
142
|
+
}
|
|
143
|
+
return `${field} {\n ${nestedSelection}\n }`;
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
.join('\n ');
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* DAB represents collection navigation properties as "Connection" objects containing an `items` array.
|
|
150
|
+
* For ergonomics (and to match our schema types like `todos?: Todo[]`), unwrap any selected nested
|
|
151
|
+
* connection objects to their `items` arrays.
|
|
152
|
+
*/
|
|
153
|
+
unwrapNestedConnectionItems(entities) {
|
|
154
|
+
if (!entities.length || this.selections.length === 0) {
|
|
155
|
+
return entities;
|
|
156
|
+
}
|
|
157
|
+
const nestedRoots = new Set(this.selections
|
|
158
|
+
.map((field) => String(field))
|
|
159
|
+
.filter((field) => field.includes('.'))
|
|
160
|
+
.map((field) => field.split('.', 2)[0]));
|
|
161
|
+
if (nestedRoots.size === 0) {
|
|
162
|
+
return entities;
|
|
163
|
+
}
|
|
164
|
+
for (const entity of entities) {
|
|
165
|
+
for (const root of nestedRoots) {
|
|
166
|
+
const value = entity?.[root];
|
|
167
|
+
if (value &&
|
|
168
|
+
typeof value === 'object' &&
|
|
169
|
+
!Array.isArray(value) &&
|
|
170
|
+
Array.isArray(value.items)) {
|
|
171
|
+
entity[root] = value.items;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return entities;
|
|
176
|
+
}
|
|
177
|
+
buildArguments(excludePagination = false) {
|
|
178
|
+
const args = [];
|
|
179
|
+
// Filter argument (DAB object format)
|
|
180
|
+
if (Object.keys(this.whereConditions).length > 0) {
|
|
181
|
+
const filterObj = this.buildFilterObject(this.whereConditions);
|
|
182
|
+
args.push(`filter: ${filterObj}`);
|
|
183
|
+
}
|
|
184
|
+
// OrderBy argument
|
|
185
|
+
if (this.orderByConditions.length > 0) {
|
|
186
|
+
const orderByArray = this.buildOrderByArray();
|
|
187
|
+
args.push(`orderBy: ${orderByArray}`);
|
|
188
|
+
}
|
|
189
|
+
if (!excludePagination) {
|
|
190
|
+
// Pagination arguments (DAB only supports forward pagination)
|
|
191
|
+
if (this.paginationConfig.first !== undefined) {
|
|
192
|
+
args.push(`first: ${this.paginationConfig.first}`);
|
|
193
|
+
}
|
|
194
|
+
if (this.paginationConfig.after !== undefined) {
|
|
195
|
+
args.push(`after: "${this.paginationConfig.after}"`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return args.length > 0 ? `(${args.join(', ')})` : '';
|
|
199
|
+
}
|
|
200
|
+
buildFilterObject(filter) {
|
|
201
|
+
const conditions = [];
|
|
202
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
203
|
+
if (key === 'and') {
|
|
204
|
+
const andConditions = value
|
|
205
|
+
.map((cond) => this.buildFilterObject(cond))
|
|
206
|
+
.join(', ');
|
|
207
|
+
conditions.push(`and: [${andConditions}]`);
|
|
208
|
+
}
|
|
209
|
+
else if (key === 'or') {
|
|
210
|
+
const orConditions = value
|
|
211
|
+
.map((cond) => this.buildFilterObject(cond))
|
|
212
|
+
.join(', ');
|
|
213
|
+
conditions.push(`or: [${orConditions}]`);
|
|
214
|
+
}
|
|
215
|
+
else if (key === 'not') {
|
|
216
|
+
const notCondition = this.buildFilterObject(value);
|
|
217
|
+
conditions.push(`not: ${notCondition}`);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
if (this.isRelationshipNullFilter(key, value)) {
|
|
221
|
+
const foreignKeyField = `${key}_${getPrimaryKeyField()}`;
|
|
222
|
+
const fieldFilter = this.buildFieldFilter(foreignKeyField, value);
|
|
223
|
+
conditions.push(`${foreignKeyField}: ${fieldFilter}`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
const fieldFilter = this.buildFieldFilter(key, value);
|
|
227
|
+
conditions.push(`${key}: ${fieldFilter}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return `{ ${conditions.join(', ')} }`;
|
|
232
|
+
}
|
|
233
|
+
buildFieldFilter(fieldName, value) {
|
|
234
|
+
if (typeof value === 'object' &&
|
|
235
|
+
!Array.isArray(value) &&
|
|
236
|
+
!(value instanceof Date)) {
|
|
237
|
+
const operators = Object.entries(value)
|
|
238
|
+
.map(([op, val]) => {
|
|
239
|
+
if (typeof val === 'object' &&
|
|
240
|
+
!Array.isArray(val) &&
|
|
241
|
+
!(val instanceof Date)) {
|
|
242
|
+
return `${op}: ${this.buildFieldFilter(fieldName, val)}`;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
return `${op}: ${this.formatValue(val)}`;
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
.join(', ');
|
|
249
|
+
return `{ ${operators} }`;
|
|
250
|
+
}
|
|
251
|
+
return `{ eq: ${this.formatValue(value)} }`;
|
|
252
|
+
}
|
|
253
|
+
isRelationshipNullFilter(key, value) {
|
|
254
|
+
if (!this.isNullFilterObject(value)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
if (EntityNameResolver.isPlural(key)) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
return this.hasNestedSelectionFor(key);
|
|
261
|
+
}
|
|
262
|
+
isNullFilterObject(value) {
|
|
263
|
+
if (typeof value !== 'object' ||
|
|
264
|
+
value === null ||
|
|
265
|
+
Array.isArray(value) ||
|
|
266
|
+
value instanceof Date) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
const entries = Object.entries(value);
|
|
270
|
+
return (entries.length === 1 &&
|
|
271
|
+
entries[0][0] === 'isNull' &&
|
|
272
|
+
typeof entries[0][1] === 'boolean');
|
|
273
|
+
}
|
|
274
|
+
hasNestedSelectionFor(key) {
|
|
275
|
+
return this.selections.some((field) => {
|
|
276
|
+
const fieldStr = String(field);
|
|
277
|
+
if (!fieldStr.includes('.')) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const [root] = fieldStr.split('.', 2);
|
|
281
|
+
return root === key;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
buildOrderByArray() {
|
|
285
|
+
// DAB expects a single OrderBy object, not an array
|
|
286
|
+
// Convert from our array format to single object format
|
|
287
|
+
const orderEntries = [];
|
|
288
|
+
for (const order of this.orderByConditions) {
|
|
289
|
+
for (const [field, direction] of Object.entries(order)) {
|
|
290
|
+
// Use enum values (ASC/DESC) not quoted strings
|
|
291
|
+
const enumValue = direction?.toUpperCase();
|
|
292
|
+
orderEntries.push(`${field}: ${enumValue}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return `{ ${orderEntries.join(', ')} }`;
|
|
296
|
+
}
|
|
297
|
+
formatValue(value) {
|
|
298
|
+
if (typeof value === 'string') {
|
|
299
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
300
|
+
}
|
|
301
|
+
if (typeof value === 'boolean') {
|
|
302
|
+
return String(value);
|
|
303
|
+
}
|
|
304
|
+
if (typeof value === 'number') {
|
|
305
|
+
return String(value);
|
|
306
|
+
}
|
|
307
|
+
if (value instanceof Date) {
|
|
308
|
+
return `"${value.toISOString()}"`;
|
|
309
|
+
}
|
|
310
|
+
if (Array.isArray(value)) {
|
|
311
|
+
return `[${value.map((v) => this.formatValue(v)).join(', ')}]`;
|
|
312
|
+
}
|
|
313
|
+
if (value === null) {
|
|
314
|
+
return 'null';
|
|
315
|
+
}
|
|
316
|
+
return `"${String(value)}"`;
|
|
317
|
+
}
|
|
318
|
+
mergeFilters(existing, additional) {
|
|
319
|
+
if (Object.keys(existing).length === 0) {
|
|
320
|
+
return additional;
|
|
321
|
+
}
|
|
322
|
+
return { and: [existing, additional] };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=GraphQLQueryBuilder.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PagedResult } from './types';
|
|
2
|
+
export declare class ResponseError extends Error {
|
|
3
|
+
readonly context: {
|
|
4
|
+
response?: any;
|
|
5
|
+
expectedStructure?: string;
|
|
6
|
+
};
|
|
7
|
+
constructor(message: string, context?: {
|
|
8
|
+
response?: any;
|
|
9
|
+
expectedStructure?: string;
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export declare class ResponseHandler {
|
|
13
|
+
/**
|
|
14
|
+
* Unwrap DAB GraphQL response structure
|
|
15
|
+
*/
|
|
16
|
+
static unwrapGraphQLResponse<T>(response: any, entityPluralName: string, expectPagination?: boolean): T[] | PagedResult<T>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=ResponseHandler.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { deserializeDabResponse } from '../utils/serialization';
|
|
2
|
+
export class ResponseError extends Error {
|
|
3
|
+
context;
|
|
4
|
+
constructor(message, context = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.context = context;
|
|
7
|
+
this.name = 'ResponseError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class ResponseHandler {
|
|
11
|
+
/**
|
|
12
|
+
* Unwrap DAB GraphQL response structure
|
|
13
|
+
*/
|
|
14
|
+
static unwrapGraphQLResponse(response, entityPluralName, expectPagination = false) {
|
|
15
|
+
// Handle case where response is already the data portion
|
|
16
|
+
let entityData;
|
|
17
|
+
if (response?.data) {
|
|
18
|
+
// Full GraphQL response format: { data: { entityName: { items: [...] } } }
|
|
19
|
+
entityData = response.data[entityPluralName];
|
|
20
|
+
}
|
|
21
|
+
else if (response?.[entityPluralName]) {
|
|
22
|
+
// Data portion only: { entityName: { items: [...] } }
|
|
23
|
+
entityData = response[entityPluralName];
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
throw new ResponseError('Invalid response structure: missing data field', {
|
|
27
|
+
response,
|
|
28
|
+
expectedStructure: '{ data: { [entityName]: { items: [...] } } } or { [entityName]: { items: [...] } }',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (!entityData) {
|
|
32
|
+
throw new ResponseError(`No data found for entity: ${entityPluralName}`, {
|
|
33
|
+
response,
|
|
34
|
+
expectedStructure: `{ data: { ${entityPluralName}: { items: [...] } } }`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
// For single item queries (e.g., findById)
|
|
38
|
+
if (!entityData.items && !Array.isArray(entityData)) {
|
|
39
|
+
return deserializeDabResponse(entityData);
|
|
40
|
+
}
|
|
41
|
+
// Validate DAB structure for list queries
|
|
42
|
+
if (!Array.isArray(entityData.items)) {
|
|
43
|
+
throw new ResponseError('Invalid DAB response: items is not an array', {
|
|
44
|
+
response: entityData,
|
|
45
|
+
expectedStructure: '{ items: [...], endCursor?: string, hasNextPage?: boolean }',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (expectPagination) {
|
|
49
|
+
// DAB returns pagination fields at the top level of the entity response
|
|
50
|
+
const paginatedResult = {
|
|
51
|
+
items: entityData.items,
|
|
52
|
+
// DAB only supports forward pagination (hasNextPage, endCursor)
|
|
53
|
+
hasNextPage: entityData.hasNextPage ?? false,
|
|
54
|
+
endCursor: entityData.endCursor ?? undefined,
|
|
55
|
+
};
|
|
56
|
+
// Apply DAB deserialization to the items
|
|
57
|
+
return {
|
|
58
|
+
...paginatedResult,
|
|
59
|
+
items: deserializeDabResponse(paginatedResult.items),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Apply DAB deserialization to the items array
|
|
63
|
+
return deserializeDabResponse(entityData.items);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=ResponseHandler.js.map
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { PrimaryKeyField } from '@microsoft/rayfin-core';
|
|
2
|
+
/**
|
|
3
|
+
* Base entity schema type for the client.
|
|
4
|
+
*/
|
|
5
|
+
export type EntitySchema = Record<string, any>;
|
|
6
|
+
/**
|
|
7
|
+
* Clean entity keys that exclude prototype methods and functions
|
|
8
|
+
*/
|
|
9
|
+
export type CleanEntityKeys<T> = keyof {
|
|
10
|
+
[K in keyof T as T[K] extends Function ? never : K]: T[K];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Nested field path support with type constraints for relationships
|
|
14
|
+
*/
|
|
15
|
+
export type NestedFieldPath<T> = T extends object ? {
|
|
16
|
+
[K in CleanEntityKeys<T>]: K extends string ? NonNullable<T[K]> extends (infer U)[] ? U extends object ? `${K}.${CleanEntityKeys<U>}` | K : K : NonNullable<T[K]> extends object ? `${K}.${CleanEntityKeys<NonNullable<T[K]>>}` | K : K : never;
|
|
17
|
+
}[CleanEntityKeys<T>] : never;
|
|
18
|
+
/**
|
|
19
|
+
* Type-safe field selection with nested paths
|
|
20
|
+
*/
|
|
21
|
+
export type FieldSelection<T> = readonly (CleanEntityKeys<T> | NestedFieldPath<T>)[];
|
|
22
|
+
/**
|
|
23
|
+
* DAB-compliant filter input
|
|
24
|
+
*/
|
|
25
|
+
export type RelationshipIsNullFilter<T> = T extends object ? T extends readonly unknown[] ? never : {
|
|
26
|
+
isNull?: boolean;
|
|
27
|
+
} : never;
|
|
28
|
+
/**
|
|
29
|
+
* Filter value that can be a direct value, a filter input, or a relationship isNull filter
|
|
30
|
+
*/
|
|
31
|
+
export type FilterValue<T, K extends keyof T> = T[K] | FilterInput<NonNullable<T[K]>> | FieldFilterInput<NonNullable<T[K]>> | (undefined extends T[K] ? RelationshipIsNullFilter<NonNullable<T[K]>> : never);
|
|
32
|
+
/**
|
|
33
|
+
* DAB-compliant filter input type
|
|
34
|
+
*/
|
|
35
|
+
export type FilterInput<T> = {
|
|
36
|
+
[K in keyof T]?: FilterValue<T, K>;
|
|
37
|
+
} & {
|
|
38
|
+
and?: FilterInput<T>[];
|
|
39
|
+
or?: FilterInput<T>[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Field-specific filter operators per DAB specification
|
|
43
|
+
*/
|
|
44
|
+
export type FieldFilterInput<T> = T extends string ? StringFilterInput : T extends number ? NumberFilterInput : T extends boolean ? BooleanFilterInput : T extends Date ? DateFilterInput : GenericFilterInput<T>;
|
|
45
|
+
export interface StringFilterInput {
|
|
46
|
+
eq?: string;
|
|
47
|
+
neq?: string;
|
|
48
|
+
gt?: string;
|
|
49
|
+
gte?: string;
|
|
50
|
+
lt?: string;
|
|
51
|
+
lte?: string;
|
|
52
|
+
contains?: string;
|
|
53
|
+
notContains?: string;
|
|
54
|
+
startsWith?: string;
|
|
55
|
+
endsWith?: string;
|
|
56
|
+
isNull?: boolean;
|
|
57
|
+
in?: string[];
|
|
58
|
+
}
|
|
59
|
+
export interface NumberFilterInput {
|
|
60
|
+
eq?: number;
|
|
61
|
+
neq?: number;
|
|
62
|
+
gt?: number;
|
|
63
|
+
gte?: number;
|
|
64
|
+
lt?: number;
|
|
65
|
+
lte?: number;
|
|
66
|
+
isNull?: boolean;
|
|
67
|
+
in?: number[];
|
|
68
|
+
}
|
|
69
|
+
export interface BooleanFilterInput {
|
|
70
|
+
eq?: boolean;
|
|
71
|
+
neq?: boolean;
|
|
72
|
+
isNull?: boolean;
|
|
73
|
+
in?: boolean[];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* WARNING: DAB currently does not support comparison operators for Date fields in PostgreSQL
|
|
77
|
+
*/
|
|
78
|
+
export interface DateFilterInput {
|
|
79
|
+
eq?: Date;
|
|
80
|
+
neq?: Date;
|
|
81
|
+
gt?: Date;
|
|
82
|
+
gte?: Date;
|
|
83
|
+
lt?: Date;
|
|
84
|
+
lte?: Date;
|
|
85
|
+
isNull?: boolean;
|
|
86
|
+
in?: Date[];
|
|
87
|
+
}
|
|
88
|
+
export interface GenericFilterInput<T> {
|
|
89
|
+
eq?: Partial<T>;
|
|
90
|
+
neq?: Partial<T>;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* DAB pagination configuration (forward pagination only).
|
|
94
|
+
*
|
|
95
|
+
* Microsoft Data API Builder only supports forward pagination with `first` and `after`.
|
|
96
|
+
* Backward pagination with `last` and `before` is not supported.
|
|
97
|
+
*
|
|
98
|
+
* @see https://learn.microsoft.com/en-us/azure/data-api-builder/keywords/after-graphql
|
|
99
|
+
* @see https://learn.microsoft.com/en-us/azure/data-api-builder/keywords/first-graphql
|
|
100
|
+
*/
|
|
101
|
+
export interface PaginationConfig {
|
|
102
|
+
/** Number of items to return per page */
|
|
103
|
+
first?: number;
|
|
104
|
+
/** Cursor to resume pagination from */
|
|
105
|
+
after?: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* DAB response wrapper
|
|
109
|
+
*/
|
|
110
|
+
export interface PagedResult<T> {
|
|
111
|
+
items: T[];
|
|
112
|
+
hasNextPage: boolean;
|
|
113
|
+
endCursor?: string;
|
|
114
|
+
totalCount?: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* DAB order by input
|
|
118
|
+
*/
|
|
119
|
+
export type OrderByInput<T> = {
|
|
120
|
+
[K in keyof T]?: 'asc' | 'desc';
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Extracts the primary key field from an entity using the centralized {@link PrimaryKeyField} type.
|
|
124
|
+
*
|
|
125
|
+
* @see {@link RelationshipInput} for usage in mutation input types
|
|
126
|
+
*/
|
|
127
|
+
export type PrimaryKeyOnly<T> = PrimaryKeyField extends keyof T ? Pick<T, PrimaryKeyField> : never;
|
|
128
|
+
/**
|
|
129
|
+
* Flexible input type for relationship fields in mutations.
|
|
130
|
+
*
|
|
131
|
+
* Accepts either:
|
|
132
|
+
* - Full entity object (current behavior, useful when you have the object)
|
|
133
|
+
* - Object with only the primary key field(s) (new ergonomic shorthand)
|
|
134
|
+
*
|
|
135
|
+
* @remarks
|
|
136
|
+
* The runtime already handles both forms via `isRelationshipObject()` which
|
|
137
|
+
* detects objects with 'id' or 'Id' properties and extracts the ID value
|
|
138
|
+
* to form the foreign key (e.g., `category_id`).
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* // Option 1: Pass full object (current behavior)
|
|
143
|
+
* await client.data.Todo.create({
|
|
144
|
+
* title: 'My Todo',
|
|
145
|
+
* category: { id: 'cat-123', name: 'Work', color: '#ff0000' },
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* // Option 2: Pass primary-key-only object (new ergonomic shorthand)
|
|
149
|
+
* await client.data.Todo.create({
|
|
150
|
+
* title: 'My Todo',
|
|
151
|
+
* category: { id: 'cat-123' },
|
|
152
|
+
* });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export type RelationshipInput<T> = T | PrimaryKeyOnly<T>;
|
|
156
|
+
/**
|
|
157
|
+
* Detects if a type is a relationship (object with a primary key that's not a primitive wrapper).
|
|
158
|
+
*
|
|
159
|
+
* @remarks
|
|
160
|
+
* A relationship is detected as an object type that has a {@link PrimaryKeyField} property,
|
|
161
|
+
* but is not a Date or Array (which are object types but not relationships).
|
|
162
|
+
*/
|
|
163
|
+
export type IsRelationship<T> = PrimaryKeyField extends keyof T ? T extends Date | Array<any> ? false : true : false;
|
|
164
|
+
/**
|
|
165
|
+
* Transforms an entity type for mutation input.
|
|
166
|
+
*
|
|
167
|
+
* - `@one` relationship fields become `RelationshipInput<T>` (accepts full object or id-only)
|
|
168
|
+
* - `@many` relationship fields (arrays) are allowed but ignored at runtime
|
|
169
|
+
* (they're managed via the FK on the "many" side, not on the parent)
|
|
170
|
+
* - Primitive fields remain unchanged
|
|
171
|
+
*
|
|
172
|
+
* @remarks
|
|
173
|
+
* This type preserves optionality: if a field is optional in the entity (e.g., `category?: Category`),
|
|
174
|
+
* it remains optional in the mutation input.
|
|
175
|
+
*
|
|
176
|
+
* `@many` arrays can be passed for convenience but will be ignored by the GraphQL mutation.
|
|
177
|
+
* To manage `@many` relationships, update the child entities' `@one` references instead.
|
|
178
|
+
*/
|
|
179
|
+
export type MutationInput<T> = {
|
|
180
|
+
[K in keyof T]: NonNullable<T[K]> extends Array<any> ? T[K] : IsRelationship<NonNullable<T[K]>> extends true ? RelationshipInput<NonNullable<T[K]>> | (undefined extends T[K] ? undefined : never) : T[K];
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Input type for creating new entities.
|
|
184
|
+
* The primary key field is optional since it's typically database-generated,
|
|
185
|
+
* but can be provided if the user wants to specify a particular id.
|
|
186
|
+
*/
|
|
187
|
+
export type CreateInput<T> = PrimaryKeyField extends keyof T ? Omit<MutationInput<T>, PrimaryKeyField> & Partial<Pick<T, PrimaryKeyField>> : MutationInput<T>;
|
|
188
|
+
/**
|
|
189
|
+
* Input type for updating existing entities.
|
|
190
|
+
*
|
|
191
|
+
* All fields are optional. Relationship fields accept full object or primary-key-only.
|
|
192
|
+
* `@many` relationship arrays are allowed but ignored at runtime.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* // Update with primary-key-only for relationship
|
|
197
|
+
* const input: UpdateInput<Todo> = {
|
|
198
|
+
* title: 'Updated Title',
|
|
199
|
+
* category: { id: 'new-cat-id' },
|
|
200
|
+
* };
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export type UpdateInput<T> = Partial<MutationInput<T>>;
|
|
204
|
+
/**
|
|
205
|
+
* Input type for uniquely identifying an entity.
|
|
206
|
+
* Uses the centralized PrimaryKeyField type alias.
|
|
207
|
+
*/
|
|
208
|
+
export type WhereUniqueInput<T> = {
|
|
209
|
+
[K in PrimaryKeyField]: string;
|
|
210
|
+
};
|
|
211
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createDataApi, DataApi } from './client/DataApi';
|
|
2
|
+
export type { TypedDataClients } from './client/DataApi';
|
|
3
|
+
export { GraphQLClient } from './graphql/GraphQLClient';
|
|
4
|
+
export { GraphQLEntityClient } from './graphql/GraphQLEntityClient';
|
|
5
|
+
export { GraphQLQueryBuilder } from './graphql/GraphQLQueryBuilder';
|
|
6
|
+
export { ResponseHandler } from './graphql/ResponseHandler';
|
|
7
|
+
export type { GraphQLRequest, GraphQLResponse } from './graphql/GraphQLClient';
|
|
8
|
+
export type { EntitySchema, CleanEntityKeys, NestedFieldPath, IsRelationship, RelationshipInput, PrimaryKeyOnly, MutationInput, CreateInput, UpdateInput, WhereUniqueInput, FilterInput, FilterValue, RelationshipIsNullFilter, FieldFilterInput, FieldSelection, OrderByInput, PaginationConfig, PagedResult, StringFilterInput, NumberFilterInput, BooleanFilterInput, DateFilterInput, GenericFilterInput, } from './graphql/types';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Main exports
|
|
2
|
+
export { createDataApi, DataApi } from './client/DataApi';
|
|
3
|
+
export { GraphQLClient } from './graphql/GraphQLClient';
|
|
4
|
+
export { GraphQLEntityClient } from './graphql/GraphQLEntityClient';
|
|
5
|
+
export { GraphQLQueryBuilder } from './graphql/GraphQLQueryBuilder';
|
|
6
|
+
// DAB utilities
|
|
7
|
+
export { ResponseHandler } from './graphql/ResponseHandler';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|