Skip to content

Commit 0169382

Browse files
Merge pull request #199 from objectstack-ai/copilot/refactor-server-api-implementation
2 parents 45fea83 + 47996aa commit 0169382

File tree

9 files changed

+101
-61
lines changed

9 files changed

+101
-61
lines changed

examples/integrations/express-server/__tests__/data-api.test.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ describe('Data API', () => {
8787
.set('Accept', 'application/json');
8888

8989
expect(response.status).toBe(200);
90-
expect(response.body.id).toBeDefined();
91-
expect(response.body.name).toBe('John Doe');
92-
expect(response.body.email).toBe('john@example.com');
93-
expect(response.body.id).toBeDefined();
90+
expect(response.body.data).toBeDefined();
91+
expect(response.body.data.id).toBeDefined();
92+
expect(response.body.data.name).toBe('John Doe');
93+
expect(response.body.data.email).toBe('john@example.com');
9494

95-
createdUserId = response.body.id;
95+
createdUserId = response.body.data.id;
9696
});
9797

9898
it('should find all users', async () => {
@@ -122,9 +122,10 @@ describe('Data API', () => {
122122
.set('Accept', 'application/json');
123123

124124
expect(response.status).toBe(200);
125-
expect(response.body.id).toBeDefined();
126-
expect(response.body.id).toBe(createdUserId);
127-
expect(response.body.name).toBe('John Doe');
125+
expect(response.body.data).toBeDefined();
126+
expect(response.body.data.id).toBeDefined();
127+
expect(response.body.data.id).toBe(createdUserId);
128+
expect(response.body.data.name).toBe('John Doe');
128129
});
129130

130131
it('should update a user', async () => {
@@ -143,7 +144,8 @@ describe('Data API', () => {
143144
.set('Accept', 'application/json');
144145

145146
expect(response.status).toBe(200);
146-
expect(response.body.id).toBeDefined();
147+
expect(response.body.data).toBeDefined();
148+
expect(response.body.data.id).toBeDefined();
147149
});
148150

149151

@@ -169,11 +171,11 @@ describe('Data API', () => {
169171
.set('Accept', 'application/json');
170172

171173
expect(response.status).toBe(200);
172-
expect(response.body.id).toBeDefined();
173-
expect(response.body.title).toBe('Test Task');
174-
expect(response.body.id).toBeDefined();
174+
expect(response.body.data).toBeDefined();
175+
expect(response.body.data.id).toBeDefined();
176+
expect(response.body.data.title).toBe('Test Task');
175177

176-
createdTaskId = response.body.id;
178+
createdTaskId = response.body.data.id;
177179
});
178180

179181
it('should find tasks with filter', async () => {
@@ -210,7 +212,8 @@ describe('Data API', () => {
210212
.set('Accept', 'application/json');
211213

212214
expect(response.status).toBe(200);
213-
expect(response.body.id).toBeDefined();
215+
expect(response.body.data).toBeDefined();
216+
expect(response.body.data.id).toBeDefined();
214217
});
215218

216219

@@ -233,11 +236,11 @@ describe('Data API', () => {
233236
.set('Accept', 'application/json');
234237

235238
expect(response.status).toBe(201);
236-
expect(response.body.id).toBeDefined();
237-
expect(response.body.id).toBeDefined();
238-
expect(response.body.name).toBe('Jane Smith');
239+
expect(response.body.data).toBeDefined();
240+
expect(response.body.data.id).toBeDefined();
241+
expect(response.body.data.name).toBe('Jane Smith');
239242

240-
userId = response.body.id;
243+
userId = response.body.data.id;
241244
});
242245

243246
it('should list users via GET /api/data/user', async () => {
@@ -267,7 +270,8 @@ describe('Data API', () => {
267270
.set('Accept', 'application/json');
268271

269272
expect(response.status).toBe(200);
270-
expect(response.body.id).toBeDefined();
273+
expect(response.body.data).toBeDefined();
274+
expect(response.body.data.id).toBeDefined();
271275
});
272276

273277
it('should delete user via DELETE /api/data/user/:id', async () => {
@@ -290,10 +294,11 @@ describe('Data API', () => {
290294
.set('Accept', 'application/json');
291295

292296
expect(response.status).toBe(201);
293-
expect(response.body.id).toBeDefined();
294-
expect(response.body.title).toBe('REST API Task');
297+
expect(response.body.data).toBeDefined();
298+
expect(response.body.data.id).toBeDefined();
299+
expect(response.body.data.title).toBe('REST API Task');
295300

296-
taskId = response.body.id;
301+
taskId = response.body.data.id;
297302
});
298303

299304
it('should list tasks via GET /api/data/task', async () => {
@@ -315,7 +320,8 @@ describe('Data API', () => {
315320
.set('Accept', 'application/json');
316321

317322
expect(response.status).toBe(200);
318-
expect(response.body.id).toBeDefined();
323+
expect(response.body.data).toBeDefined();
324+
expect(response.body.data.id).toBeDefined();
319325
});
320326

321327
it('should delete task via DELETE /api/data/task/:id', async () => {

packages/foundation/types/src/api.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,22 @@ export interface DataApiListResponse<T = unknown> extends DataApiResponse<T> {
126126

127127
/**
128128
* Response for single item operations (findOne, create, update)
129+
* Single item operations wrap the data in a 'data' field
129130
*/
130131
export interface DataApiItemResponse<T = unknown> extends DataApiResponse<T> {
131-
/** The item ID */
132-
_id?: string | number;
133-
/** Object type identifier */
134-
'@type'?: string;
135-
/** Timestamp when created */
136-
created_at?: string | Date;
137-
/** Timestamp when last updated */
138-
updated_at?: string | Date;
132+
/** The returned item data */
133+
data?: T & {
134+
/** The item ID */
135+
_id?: string | number;
136+
/** Object type identifier */
137+
'@type'?: string;
138+
/** Timestamp when created */
139+
created_at?: string | Date;
140+
/** Timestamp when last updated */
141+
updated_at?: string | Date;
142+
/** Additional item fields */
143+
[key: string]: unknown;
144+
};
139145
}
140146

141147
/**

packages/runtime/server/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,21 @@ List operations return data in an `items` array with optional pagination metadat
9393

9494
### Single Item Operations (findOne, create, update, delete)
9595

96-
Single item operations return data in a `data` field:
96+
Single item operations return data in a `data` field with an `@type` identifier:
9797

9898
```json
9999
{
100100
"data": {
101-
"id": "1001",
101+
"_id": "1001",
102102
"name": "Contract A",
103-
"amount": 5000
103+
"amount": 5000,
104+
"@type": "contract"
104105
}
105106
}
106107
```
107108

109+
**Note:** The `@type` field indicates the object type, and `_id` is the unique identifier.
110+
108111
### Error Responses
109112

110113
All errors follow a consistent format:

packages/runtime/server/src/adapters/graphql.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ export function generateGraphQLSchema(app: IObjectQL): GraphQLSchema {
240240
throw new Error(result.error.message);
241241
}
242242

243-
return normalizeId(result);
243+
// Extract data from the response wrapper
244+
return normalizeId(result.data);
244245
}
245246
};
246247

@@ -322,7 +323,8 @@ export function generateGraphQLSchema(app: IObjectQL): GraphQLSchema {
322323
throw new Error(result.error.message);
323324
}
324325

325-
return normalizeId(result);
326+
// Extract data from the response wrapper
327+
return normalizeId(result.data);
326328
}
327329
};
328330

@@ -347,7 +349,8 @@ export function generateGraphQLSchema(app: IObjectQL): GraphQLSchema {
347349
throw new Error(result.error.message);
348350
}
349351

350-
return normalizeId(result);
352+
// Extract data from the response wrapper
353+
return normalizeId(result.data);
351354
}
352355
};
353356

@@ -368,7 +371,8 @@ export function generateGraphQLSchema(app: IObjectQL): GraphQLSchema {
368371
throw new Error(result.error.message);
369372
}
370373

371-
return result;
374+
// Extract data from the response wrapper for delete
375+
return result.data;
372376
}
373377
};
374378
}

packages/runtime/server/src/adapters/rest.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,17 @@ export function createRESTHandler(app: IObjectQL, options?: RESTHandlerOptions)
291291
return;
292292
}
293293

294+
// Check if single item operations returned null data
295+
if (qlRequest.op === 'findOne' && result.data === null) {
296+
sendJSON(res, 404, {
297+
error: {
298+
code: ErrorCode.NOT_FOUND,
299+
message: 'Resource not found'
300+
}
301+
});
302+
return;
303+
}
304+
294305
// Determine HTTP status code
295306
let statusCode = 200;
296307
if (result.error) {

packages/runtime/server/src/server.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,21 @@ export class ObjectQLServer {
7272
// Support both string ID and query object
7373
result = await repo.findOne(req.args);
7474
if (result) {
75-
return { ...result, '@type': req.object };
75+
return { data: { ...result, '@type': req.object } };
7676
}
77-
return result;
77+
return { data: null };
7878
case 'create':
7979
result = await repo.create(req.args);
8080
if (result) {
81-
return { ...result, '@type': req.object };
81+
return { data: { ...result, '@type': req.object } };
8282
}
83-
return result;
83+
return { data: null };
8484
case 'update':
8585
result = await repo.update(req.args.id, req.args.data);
8686
if (result) {
87-
return { ...result, '@type': req.object };
87+
return { data: { ...result, '@type': req.object } };
8888
}
89-
return result;
89+
return { data: null };
9090
case 'delete':
9191
result = await repo.delete(req.args.id);
9292
if (!result) {
@@ -95,11 +95,13 @@ export class ObjectQLServer {
9595
`Record with id '${req.args.id}' not found for delete`
9696
);
9797
}
98-
// Return standardized delete response with object type
98+
// Return standardized delete response with data wrapper
9999
return {
100-
id: req.args.id,
101-
deleted: true,
102-
'@type': req.object
100+
data: {
101+
id: req.args.id,
102+
deleted: true,
103+
'@type': req.object
104+
}
103105
};
104106
case 'count':
105107
result = await repo.count(req.args);

packages/runtime/server/test/node.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,11 @@ describe('Node Adapter', () => {
8888

8989
expect(response.status).toBe(200);
9090
expect(response.body).toEqual({
91-
id: 2,
92-
name: 'Bob',
93-
'@type': 'user'
91+
data: {
92+
id: 2,
93+
name: 'Bob',
94+
'@type': 'user'
95+
}
9496
});
9597
});
9698

packages/runtime/server/test/rest-advanced.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,8 @@ describe('REST API Error Handling & Edge Cases', () => {
291291
.set('Accept', 'application/json');
292292

293293
expect(response.status).toBe(200);
294-
expect(response.body['@type']).toBe('task');
294+
expect(response.body.data).toBeDefined();
295+
expect(response.body.data['@type']).toBe('task');
295296
});
296297

297298
it('should include @type in created record response', async () => {
@@ -301,7 +302,8 @@ describe('REST API Error Handling & Edge Cases', () => {
301302
.set('Accept', 'application/json');
302303

303304
expect(response.status).toBe(201);
304-
expect(response.body['@type']).toBe('task');
305+
expect(response.body.data).toBeDefined();
306+
expect(response.body.data['@type']).toBe('task');
305307
});
306308

307309
it('should return items array for list endpoint', async () => {

packages/runtime/server/test/rest.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,9 @@ describe('REST API Adapter', () => {
291291
.set('Accept', 'application/json');
292292

293293
expect(response.status).toBe(200);
294-
expect(response.body.name).toBe('Alice');
295-
expect(response.body['@type']).toBe('user');
294+
expect(response.body.data).toBeDefined();
295+
expect(response.body.data.name).toBe('Alice');
296+
expect(response.body.data['@type']).toBe('user');
296297
});
297298

298299
it('should handle POST /api/data/:object - Create record', async () => {
@@ -302,9 +303,10 @@ describe('REST API Adapter', () => {
302303
.set('Accept', 'application/json');
303304

304305
expect(response.status).toBe(201);
305-
expect(response.body.name).toBe('Charlie');
306-
expect(response.body._id).toBeDefined();
307-
expect(response.body['@type']).toBe('user');
306+
expect(response.body.data).toBeDefined();
307+
expect(response.body.data.name).toBe('Charlie');
308+
expect(response.body.data._id).toBeDefined();
309+
expect(response.body.data['@type']).toBe('user');
308310
});
309311

310312
it('should handle PUT /api/data/:object/:id - Update record', async () => {
@@ -322,8 +324,9 @@ describe('REST API Adapter', () => {
322324
.set('Accept', 'application/json');
323325

324326
expect(response.status).toBe(200);
325-
expect(response.body.deleted).toBe(true);
326-
expect(response.body['@type']).toBe('user');
327+
expect(response.body.data).toBeDefined();
328+
expect(response.body.data.deleted).toBe(true);
329+
expect(response.body.data['@type']).toBe('user');
327330
});
328331

329332
it('should return 404 for non-existent object', async () => {
@@ -510,7 +513,8 @@ describe('REST API Adapter', () => {
510513

511514
// Should still work as single create
512515
expect(response.status).toBe(201);
513-
expect(response.body._id).toBeDefined();
516+
expect(response.body.data).toBeDefined();
517+
expect(response.body.data._id).toBeDefined();
514518
});
515519
});
516520
});

0 commit comments

Comments
 (0)