Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 64 additions & 44 deletions packages/server/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const urlPatterns = {
relationship: new UrlPattern('/:type/:id/relationships/:relationship'),
};

export const idDivider = '_';
export const compoundIdKey = 'compoundId';

/**
* Request handler options
*/
Expand Down Expand Up @@ -59,9 +62,13 @@ type RelationshipInfo = {
isOptional: boolean;
};

type IdField = {
name: string;
type: string;
};

type ModelInfo = {
idField: string;
idFieldType: string;
idFields: IdField[];
fields: Record<string, FieldInfo>;
relationships: Record<string, RelationshipInfo>;
};
Expand Down Expand Up @@ -129,10 +136,6 @@ class RequestHandler extends APIHandlerBase {
status: 400,
title: 'Model without an ID field is not supported',
},
multiId: {
status: 400,
title: 'Model with multiple ID fields is not supported',
},
invalidId: {
status: 400,
title: 'Resource ID is invalid',
Expand Down Expand Up @@ -387,7 +390,7 @@ class RequestHandler extends APIHandlerBase {
return this.makeUnsupportedModelError(type);
}

const args: any = { where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId) };
const args: any = { where: this.makeIdFilter(typeInfo.idFields, resourceId) };

// include IDs of relation fields so that they can be serialized
this.includeRelationshipIds(type, args, 'include');
Expand All @@ -405,7 +408,12 @@ class RequestHandler extends APIHandlerBase {
include = allIncludes;
}

const entity = await prisma[type].findUnique(args);
let entity = await prisma[type].findUnique(args);

if (typeInfo.idFields.length > 1) {
entity = { ...entity, [compoundIdKey]: resourceId };
}

if (entity) {
return {
status: 200,
Expand Down Expand Up @@ -451,7 +459,7 @@ class RequestHandler extends APIHandlerBase {

select = select ?? { [relationship]: true };
const args: any = {
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
where: this.makeIdFilter(typeInfo.idFields, resourceId),
select,
};

Expand Down Expand Up @@ -510,7 +518,7 @@ class RequestHandler extends APIHandlerBase {
}

const args: any = {
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
where: this.makeIdFilter(typeInfo.idFields, resourceId),
select: this.makeIdSelect(type, modelMeta),
};

Expand Down Expand Up @@ -801,8 +809,11 @@ class RequestHandler extends APIHandlerBase {
}

const updateArgs: any = {
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
select: { [typeInfo.idField]: true, [relationship]: { select: { [relationInfo.idField]: true } } },
where: this.makeIdFilter(typeInfo.idFields, resourceId),
select: {
...typeInfo.idFields.reduce((acc, field) => ({ ...acc, [field.name]: true }), {}),
[relationship]: { select: { [relationInfo.idField]: true } },
},
};

if (!relationInfo.isCollection) {
Expand Down Expand Up @@ -897,7 +908,7 @@ class RequestHandler extends APIHandlerBase {
}

const updatePayload: any = {
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
where: this.makeIdFilter(typeInfo.idFields, resourceId),
data: { ...attributes },
};

Expand Down Expand Up @@ -948,7 +959,7 @@ class RequestHandler extends APIHandlerBase {
}

await prisma[type].delete({
where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId),
where: this.makeIdFilter(typeInfo.idFields, resourceId),
});
return {
status: 204,
Expand All @@ -966,14 +977,9 @@ class RequestHandler extends APIHandlerBase {
logWarning(logger, `Not including model ${model} in the API because it has no ID field`);
continue;
}
if (idFields.length > 1) {
logWarning(logger, `Not including model ${model} in the API because it has multiple ID fields`);
continue;
}

this.typeMap[model] = {
idField: idFields[0].name,
idFieldType: idFields[0].type,
idFields,
relationships: {},
fields,
};
Expand All @@ -990,18 +996,15 @@ class RequestHandler extends APIHandlerBase {
);
continue;
}
if (fieldTypeIdFields.length > 1) {
logWarning(
logger,
`Not including relation ${model}.${field} in the API because it has multiple ID fields`
);
continue;
}

// TODO: Multi id relationship support
const idField = fieldTypeIdFields.length > 1 ? 'id' : fieldTypeIdFields[0].name;
const idFieldType = fieldTypeIdFields.length > 1 ? 'string' : fieldTypeIdFields[0].type;

this.typeMap[model].relationships[field] = {
type: fieldInfo.type,
idField: fieldTypeIdFields[0].name,
idFieldType: fieldTypeIdFields[0].type,
idField,
idFieldType,
isCollection: !!fieldInfo.isArray,
isOptional: !!fieldInfo.isOptional,
};
Expand All @@ -1019,7 +1022,8 @@ class RequestHandler extends APIHandlerBase {

for (const model of Object.keys(modelMeta.models)) {
const ids = getIdFields(modelMeta, model);
if (ids.length !== 1) {

if (ids.length < 1) {
continue;
}

Expand All @@ -1042,7 +1046,7 @@ class RequestHandler extends APIHandlerBase {

const serializer = new Serializer(model, {
version: '1.1',
idKey: ids[0].name,
idKey: ids.length > 1 ? compoundIdKey : ids[0].name,
linkers: {
resource: linker,
document: linker,
Expand All @@ -1069,7 +1073,7 @@ class RequestHandler extends APIHandlerBase {
continue;
}
const fieldIds = getIdFields(modelMeta, fieldMeta.type);
if (fieldIds.length === 1) {
if (fieldIds.length > 0) {
const relator = new Relator(
async (data) => {
return (data as any)[field];
Expand Down Expand Up @@ -1107,10 +1111,10 @@ class RequestHandler extends APIHandlerBase {
return undefined;
}
const ids = getIdFields(modelMeta, model);
if (ids.length === 1) {
return data[ids[0].name];
} else {
if (ids.length === 0) {
return undefined;
} else {
return data[ids.map((id) => id.name).join(idDivider)];
}
}

Expand Down Expand Up @@ -1178,18 +1182,31 @@ class RequestHandler extends APIHandlerBase {
return r.toString();
}

private makeIdFilter(idField: string, idFieldType: string, resourceId: string) {
return { [idField]: this.coerce(idFieldType, resourceId) };
private makeIdFilter(idFields: IdField[], resourceId: string) {
if (idFields.length === 1) {
return { [idFields[0].name]: this.coerce(idFields[0].type, resourceId) };
} else {
return {
[idFields.map((idf) => idf.name).join('_')]: idFields.reduce(
(acc, curr, idx) => ({
...acc,
[curr.name]: this.coerce(curr.type, resourceId.split(idDivider)[idx]),
}),
{}
),
};
}
}

private makeIdSelect(model: string, modelMeta: ModelMeta) {
const idFields = getIdFields(modelMeta, model);
if (idFields.length === 0) {
throw this.errors.noId;
} else if (idFields.length > 1) {
throw this.errors.multiId;
} else if (idFields.length === 1) {
return { [idFields[0].name]: true };
} else {
return { [idFields.map((idf) => idf.name).join(',')]: true };
}
return { [idFields[0].name]: true };
}

private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') {
Expand Down Expand Up @@ -1425,7 +1442,10 @@ class RequestHandler extends APIHandlerBase {
if (!relationType) {
return { sort: undefined, error: this.makeUnsupportedModelError(fieldInfo.type) };
}
curr[fieldInfo.name] = { [relationType.idField]: dir };
curr[fieldInfo.name] = relationType.idFields.reduce((acc: any, idField: IdField) => {
acc[idField.name] = dir;
return acc;
}, {});
} else {
// regular field
curr[fieldInfo.name] = dir;
Expand Down Expand Up @@ -1509,11 +1529,11 @@ class RequestHandler extends APIHandlerBase {
const values = value.split(',').filter((i) => i);
const filterValue =
values.length > 1
? { OR: values.map((v) => this.makeIdFilter(info.idField, info.idFieldType, v)) }
: this.makeIdFilter(info.idField, info.idFieldType, value);
? { OR: values.map((v) => this.makeIdFilter(info.idFields, v)) }
: this.makeIdFilter(info.idFields, value);
return { some: filterValue };
} else {
return { is: this.makeIdFilter(info.idField, info.idFieldType, value) };
return { is: this.makeIdFilter(info.idFields, value) };
}
} else {
const coerced = this.coerce(fieldInfo.type, value);
Expand Down
38 changes: 37 additions & 1 deletion packages/server/tests/api/rest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CrudFailureReason, type ModelMeta } from '@zenstackhq/runtime';
import { loadSchema, run } from '@zenstackhq/testtools';
import { Decimal } from 'decimal.js';
import SuperJSON from 'superjson';
import makeHandler from '../../src/api/rest';
import makeHandler, { idDivider } from '../../src/api/rest';

describe('REST server tests', () => {
let prisma: any;
Expand Down Expand Up @@ -63,6 +63,13 @@ describe('REST server tests', () => {
post Post @relation(fields: [postId], references: [id])
postId Int @unique
}

model PostLike {
postId Int
userId String
superLike Boolean
@@id([postId, userId])
}
`;

beforeAll(async () => {
Expand Down Expand Up @@ -1283,6 +1290,35 @@ describe('REST server tests', () => {
next: null,
});
});

it('compound id', async () => {
await prisma.user.create({
data: { myId: 'user1', email: '[email protected]', posts: { create: { title: 'Post1' } } },
});
await prisma.user.create({
data: { myId: 'user2', email: '[email protected]' },
});
await prisma.postLike.create({
data: { userId: 'user2', postId: 1, superLike: false },
});

const r = await handler({
method: 'get',
path: `/postLike/1${idDivider}user2`, // Order of ids is same as in the model @@id
prisma,
});

console.log(r.body);

expect(r.status).toBe(200);
expect(r.body).toMatchObject({
data: {
type: 'postLike',
id: `1${idDivider}user2`,
attributes: { userId: 'user2', postId: 1, superLike: false },
},
});
});
});

describe('POST', () => {
Expand Down