Skip to content
This repository was archived by the owner on Mar 1, 2026. It is now read-only.

Commit 8d161a2

Browse files
committed
add support for mapped unique keys in externalIdMapping
1 parent c7ad7d7 commit 8d161a2

2 files changed

Lines changed: 110 additions & 3 deletions

File tree

packages/server/src/api/rest/index.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1338,8 +1338,41 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
13381338
return Object.values(modelDef.fields).filter((f) => modelDef.idFields.includes(f.name));
13391339
}
13401340

1341-
// map external ID name to unique constraint field
13421341
const externalIdName = this.externalIdMapping[modelLower];
1342+
1343+
// 1. Model-level unique indexes with @@unique(map: ...)
1344+
if (Array.isArray(modelDef.attributes)) {
1345+
const uniqueAttr = modelDef.attributes.find(
1346+
(attr: any) =>
1347+
attr.name === '@@unique' &&
1348+
Array.isArray(attr.args) &&
1349+
attr.args.some((a: any) => a.name === 'map' && a.value?.value === externalIdName),
1350+
);
1351+
if (uniqueAttr) {
1352+
const fieldsArg = uniqueAttr.args.find((a: any) => a.name === 'fields');
1353+
if (fieldsArg && Array.isArray(fieldsArg.value?.items) && fieldsArg.value.items.length >= 1) {
1354+
const fieldNames = fieldsArg.value.items.map((item: any) => {
1355+
let fieldName: string | undefined = undefined;
1356+
if (typeof item.field === 'string' && modelDef.fields[item.field]) {
1357+
fieldName = item.field;
1358+
} else if (
1359+
typeof item.value === 'object' &&
1360+
typeof item.value.name === 'string' &&
1361+
modelDef.fields[item.value.name]
1362+
) {
1363+
fieldName = item.value.name;
1364+
}
1365+
if (!fieldName) {
1366+
throw new Error(`Invalid unique field mapping in model ${model}: ${JSON.stringify(item)}`);
1367+
}
1368+
return fieldName;
1369+
});
1370+
return fieldNames.map((fieldName: string) => this.requireField(model, fieldName));
1371+
}
1372+
}
1373+
}
1374+
1375+
// 2. uniqueFields by key name
13431376
for (const [name, info] of Object.entries(modelDef.uniqueFields)) {
13441377
if (name === externalIdName) {
13451378
if (typeof info.type === 'string') {
@@ -1352,6 +1385,24 @@ export class RestApiHandler<Schema extends SchemaDef = SchemaDef> implements Api
13521385
}
13531386
}
13541387

1388+
// 3. Field-level unique attribute with @unique(map: ...)
1389+
const field = Object.values(modelDef.fields).find((fieldDef) => {
1390+
if (!Array.isArray(fieldDef.attributes)) {
1391+
return false;
1392+
}
1393+
// @unique(map: ...)
1394+
return fieldDef.attributes.some(
1395+
(attr: any) =>
1396+
attr.name === '@unique' &&
1397+
Array.isArray(attr.args) &&
1398+
attr.args.some((a: any) => a.name === 'map' && a.value?.value === externalIdName),
1399+
);
1400+
});
1401+
1402+
if (field) {
1403+
return [field];
1404+
}
1405+
13551406
throw new Error(`Model ${model} does not have unique key ${externalIdName}`);
13561407
}
13571408

packages/server/test/api/rest.test.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,6 +3185,28 @@ describe('REST server tests', () => {
31853185
title String
31863186
author User? @relation(fields: [authorId], references: [id])
31873187
authorId Int?
3188+
comments Comment[]
3189+
images Image[]
3190+
}
3191+
3192+
model Comment {
3193+
3194+
id Int @id @default(autoincrement())
3195+
content String
3196+
short_title String
3197+
post_id Int
3198+
post Post @relation(fields: [post_id], references: [id])
3199+
3200+
@@unique([short_title, post_id], map: "short_title_post")
3201+
}
3202+
3203+
model Image {
3204+
3205+
id Int @id @default(autoincrement())
3206+
content String
3207+
short_title String @unique(map: "image_short_title")
3208+
post_id Int
3209+
post Post @relation(fields: [post_id], references: [id])
31883210
}
31893211
`;
31903212
beforeEach(async () => {
@@ -3195,16 +3217,27 @@ describe('REST server tests', () => {
31953217
endpoint: 'http://localhost/api',
31963218
externalIdMapping: {
31973219
User: 'name_source',
3220+
Image: 'image_short_title',
3221+
Comment: 'short_title_post',
31983222
},
31993223
});
32003224
handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) });
3201-
});
32023225

3203-
it('works with id mapping', async () => {
32043226
await client.user.create({
32053227
data: { id: 1, name: 'User1', source: 'a' },
32063228
});
3229+
await client.post.create({
3230+
data: { id: 2, title: 'Some title', authorId: 1 },
3231+
});
3232+
await client.comment.create({
3233+
data: { id: 3, short_title: 'comment-title', post_id: 2, content: 'Comment1' },
3234+
});
3235+
await client.image.create({
3236+
data: { id: 1, short_title: 'image-title', post_id: 2, content: 'Image1' },
3237+
});
3238+
});
32073239

3240+
it('works with id mapping', async () => {
32083241
// user is no longer exposed using the `id` field
32093242
let r = await handler({
32103243
method: 'get',
@@ -3248,6 +3281,29 @@ describe('REST server tests', () => {
32483281
id: 'User1_a',
32493282
});
32503283
});
3284+
3285+
it('works with externalIdMapping as single column with underscore (single unique index)', async () => {
3286+
const r = await handler({
3287+
method: 'get',
3288+
path: '/image/image-title',
3289+
query: {},
3290+
client,
3291+
});
3292+
expect(r.status).toBe(200);
3293+
expect(r.body.data.attributes.content).toBe('Image1');
3294+
});
3295+
3296+
it('works with externalIdMapping using mapped compound unique name', async () => {
3297+
const r = await handler({
3298+
method: 'get',
3299+
path: '/comment/comment-title_2',
3300+
query: {},
3301+
client,
3302+
});
3303+
expect(r.status).toBe(200);
3304+
expect(r.body.data.attributes.short_title).toBe('comment-title');
3305+
expect(r.body.data.attributes.post_id).toBe(2);
3306+
});
32513307
});
32523308

32533309
describe('REST server tests - procedures', () => {

0 commit comments

Comments
 (0)