Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 committed Oct 19, 2024
2 parents f9ee993 + cef0e8f commit c39f596
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 57 deletions.
3 changes: 0 additions & 3 deletions packages/schema/src/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,6 @@ export function resolveImportUri(imp: ModelImport): URI | undefined {
? path.dirname(imp.$container.$document.uri.fsPath)
: process.cwd();
imp.path = findNodeModulesFile(imp.path, contextPath) ?? imp.path;
if (imp.path) {
console.log('Loaded import from:', imp.path);
}
}

const dirUri = Utils.dirname(getDocument(imp).uri);
Expand Down
122 changes: 76 additions & 46 deletions packages/server/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,6 @@ import { LoggerConfig, Response } from '../../types';
import { APIHandlerBase, RequestContext } from '../base';
import { logWarning, registerCustomSerializers } from '../utils';

const urlPatterns = {
// collection operations
collection: new UrlPattern('/:type'),
// single resource operations
single: new UrlPattern('/:type/:id'),
// related entity fetching
fetchRelationship: new UrlPattern('/:type/:id/:relationship'),
// relationship operations
relationship: new UrlPattern('/:type/:id/relationships/:relationship'),
};

export const idDivider = '_';

/**
* Request handler options
*/
Expand All @@ -52,6 +39,19 @@ export type Options = {
* Defaults to 100. Set to Infinity to disable pagination.
*/
pageSize?: number;

/**
* The divider used to separate compound ID fields in the URL.
* Defaults to '_'.
*/
idDivider?: string;

/**
* The charset used for URL segment values. Defaults to `a-zA-Z0-9-_~ %`. You can change it if your entity's ID values
* allow different characters. Specifically, if your models use compound IDs and the idDivider is set to a different value,
* it should be included in the charset.
*/
urlSegmentCharset?: string;
};

type RelationshipInfo = {
Expand Down Expand Up @@ -93,6 +93,8 @@ const FilterOperations = [

type FilterOperationType = (typeof FilterOperations)[number] | undefined;

const prismaIdDivider = '_';

registerCustomSerializers();

/**
Expand Down Expand Up @@ -210,8 +212,30 @@ class RequestHandler extends APIHandlerBase {
// all known types and their metadata
private typeMap: Record<string, ModelInfo>;

// divider used to separate compound ID fields
private idDivider;

private urlPatterns;

constructor(private readonly options: Options) {
super();
this.idDivider = options.idDivider ?? prismaIdDivider;
const segmentCharset = options.urlSegmentCharset ?? 'a-zA-Z0-9-_~ %';
this.urlPatterns = this.buildUrlPatterns(this.idDivider, segmentCharset);
}

buildUrlPatterns(idDivider: string, urlSegmentNameCharset: string) {
const options = { segmentValueCharset: urlSegmentNameCharset };
return {
// collection operations
collection: new UrlPattern('/:type', options),
// single resource operations
single: new UrlPattern('/:type/:id', options),
// related entity fetching
fetchRelationship: new UrlPattern('/:type/:id/:relationship', options),
// relationship operations
relationship: new UrlPattern('/:type/:id/relationships/:relationship', options),
};
}

async handleRequest({
Expand Down Expand Up @@ -245,19 +269,19 @@ class RequestHandler extends APIHandlerBase {
try {
switch (method) {
case 'GET': {
let match = urlPatterns.single.match(path);
let match = this.urlPatterns.single.match(path);
if (match) {
// single resource read
return await this.processSingleRead(prisma, match.type, match.id, query);
}

match = urlPatterns.fetchRelationship.match(path);
match = this.urlPatterns.fetchRelationship.match(path);
if (match) {
// fetch related resource(s)
return await this.processFetchRelated(prisma, match.type, match.id, match.relationship, query);
}

match = urlPatterns.relationship.match(path);
match = this.urlPatterns.relationship.match(path);
if (match) {
// read relationship
return await this.processReadRelationship(
Expand All @@ -269,7 +293,7 @@ class RequestHandler extends APIHandlerBase {
);
}

match = urlPatterns.collection.match(path);
match = this.urlPatterns.collection.match(path);
if (match) {
// collection read
return await this.processCollectionRead(prisma, match.type, query);
Expand All @@ -283,13 +307,13 @@ class RequestHandler extends APIHandlerBase {
return this.makeError('invalidPayload');
}

let match = urlPatterns.collection.match(path);
let match = this.urlPatterns.collection.match(path);
if (match) {
// resource creation
return await this.processCreate(prisma, match.type, query, requestBody, modelMeta, zodSchemas);
}

match = urlPatterns.relationship.match(path);
match = this.urlPatterns.relationship.match(path);
if (match) {
// relationship creation (collection relationship only)
return await this.processRelationshipCRUD(
Expand All @@ -313,7 +337,7 @@ class RequestHandler extends APIHandlerBase {
return this.makeError('invalidPayload');
}

let match = urlPatterns.single.match(path);
let match = this.urlPatterns.single.match(path);
if (match) {
// resource update
return await this.processUpdate(
Expand All @@ -327,7 +351,7 @@ class RequestHandler extends APIHandlerBase {
);
}

match = urlPatterns.relationship.match(path);
match = this.urlPatterns.relationship.match(path);
if (match) {
// relationship update
return await this.processRelationshipCRUD(
Expand All @@ -345,13 +369,13 @@ class RequestHandler extends APIHandlerBase {
}

case 'DELETE': {
let match = urlPatterns.single.match(path);
let match = this.urlPatterns.single.match(path);
if (match) {
// resource deletion
return await this.processDelete(prisma, match.type, match.id);
}

match = urlPatterns.relationship.match(path);
match = this.urlPatterns.relationship.match(path);
if (match) {
// relationship deletion (collection relationship only)
return await this.processRelationshipCRUD(
Expand Down Expand Up @@ -391,7 +415,7 @@ class RequestHandler extends APIHandlerBase {
return this.makeUnsupportedModelError(type);
}

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

// include IDs of relation fields so that they can be serialized
this.includeRelationshipIds(type, args, 'include');
Expand Down Expand Up @@ -456,7 +480,7 @@ class RequestHandler extends APIHandlerBase {

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

Expand Down Expand Up @@ -514,7 +538,7 @@ class RequestHandler extends APIHandlerBase {
}

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

Expand Down Expand Up @@ -753,7 +777,7 @@ class RequestHandler extends APIHandlerBase {
if (relationInfo.isCollection) {
createPayload.data[key] = {
connect: enumerate(data.data).map((item: any) => ({
[this.makeIdKey(relationInfo.idFields)]: item.id,
[this.makePrismaIdKey(relationInfo.idFields)]: item.id,
})),
};
} else {
Expand All @@ -762,15 +786,15 @@ class RequestHandler extends APIHandlerBase {
}
createPayload.data[key] = {
connect: {
[this.makeIdKey(relationInfo.idFields)]: data.data.id,
[this.makePrismaIdKey(relationInfo.idFields)]: data.data.id,
},
};
}

// make sure ID fields are included for result serialization
createPayload.include = {
...createPayload.include,
[key]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } },
[key]: { select: { [this.makePrismaIdKey(relationInfo.idFields)]: true } },
};
}
}
Expand Down Expand Up @@ -807,7 +831,7 @@ class RequestHandler extends APIHandlerBase {
}

const updateArgs: any = {
where: this.makeIdFilter(typeInfo.idFields, resourceId),
where: this.makePrismaIdFilter(typeInfo.idFields, resourceId),
select: {
...typeInfo.idFields.reduce((acc, field) => ({ ...acc, [field.name]: true }), {}),
[relationship]: { select: this.makeIdSelect(relationInfo.idFields) },
Expand Down Expand Up @@ -842,7 +866,7 @@ class RequestHandler extends APIHandlerBase {
updateArgs.data = {
[relationship]: {
connect: {
[this.makeIdKey(relationInfo.idFields)]: parsed.data.data.id,
[this.makePrismaIdKey(relationInfo.idFields)]: parsed.data.data.id,
},
},
};
Expand All @@ -866,7 +890,7 @@ class RequestHandler extends APIHandlerBase {
updateArgs.data = {
[relationship]: {
[relationVerb]: enumerate(parsed.data.data).map((item: any) =>
this.makeIdFilter(relationInfo.idFields, item.id)
this.makePrismaIdFilter(relationInfo.idFields, item.id)
),
},
};
Expand Down Expand Up @@ -907,7 +931,7 @@ class RequestHandler extends APIHandlerBase {
}

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

Expand All @@ -926,7 +950,7 @@ class RequestHandler extends APIHandlerBase {
if (relationInfo.isCollection) {
updatePayload.data[key] = {
set: enumerate(data.data).map((item: any) => ({
[this.makeIdKey(relationInfo.idFields)]: item.id,
[this.makePrismaIdKey(relationInfo.idFields)]: item.id,
})),
};
} else {
Expand All @@ -935,13 +959,13 @@ class RequestHandler extends APIHandlerBase {
}
updatePayload.data[key] = {
set: {
[this.makeIdKey(relationInfo.idFields)]: data.data.id,
[this.makePrismaIdKey(relationInfo.idFields)]: data.data.id,
},
};
}
updatePayload.include = {
...updatePayload.include,
[key]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } },
[key]: { select: { [this.makePrismaIdKey(relationInfo.idFields)]: true } },
};
}
}
Expand All @@ -960,7 +984,7 @@ class RequestHandler extends APIHandlerBase {
}

await prisma[type].delete({
where: this.makeIdFilter(typeInfo.idFields, resourceId),
where: this.makePrismaIdFilter(typeInfo.idFields, resourceId),
});
return {
status: 204,
Expand Down Expand Up @@ -1110,7 +1134,7 @@ class RequestHandler extends APIHandlerBase {
if (ids.length === 0) {
return undefined;
} else {
return data[ids.map((id) => id.name).join(idDivider)];
return data[this.makeIdKey(ids)];
}
}

Expand Down Expand Up @@ -1206,15 +1230,16 @@ class RequestHandler extends APIHandlerBase {
return r.toString();
}

private makeIdFilter(idFields: FieldInfo[], resourceId: string) {
private makePrismaIdFilter(idFields: FieldInfo[], resourceId: string) {
if (idFields.length === 1) {
return { [idFields[0].name]: this.coerce(idFields[0].type, resourceId) };
} else {
return {
[idFields.map((idf) => idf.name).join(idDivider)]: idFields.reduce(
// TODO: support `@@id` with custom name
[idFields.map((idf) => idf.name).join(prismaIdDivider)]: idFields.reduce(
(acc, curr, idx) => ({
...acc,
[curr.name]: this.coerce(curr.type, resourceId.split(idDivider)[idx]),
[curr.name]: this.coerce(curr.type, resourceId.split(this.idDivider)[idx]),
}),
{}
),
Expand All @@ -1230,11 +1255,16 @@ class RequestHandler extends APIHandlerBase {
}

private makeIdKey(idFields: FieldInfo[]) {
return idFields.map((idf) => idf.name).join(idDivider);
return idFields.map((idf) => idf.name).join(this.idDivider);
}

private makePrismaIdKey(idFields: FieldInfo[]) {
// TODO: support `@@id` with custom name
return idFields.map((idf) => idf.name).join(prismaIdDivider);
}

private makeCompoundId(idFields: FieldInfo[], item: any) {
return idFields.map((idf) => item[idf.name]).join(idDivider);
return idFields.map((idf) => item[idf.name]).join(this.idDivider);
}

private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') {
Expand Down Expand Up @@ -1557,11 +1587,11 @@ class RequestHandler extends APIHandlerBase {
const values = value.split(',').filter((i) => i);
const filterValue =
values.length > 1
? { OR: values.map((v) => this.makeIdFilter(info.idFields, v)) }
: this.makeIdFilter(info.idFields, value);
? { OR: values.map((v) => this.makePrismaIdFilter(info.idFields, v)) }
: this.makePrismaIdFilter(info.idFields, value);
return { some: filterValue };
} else {
return { is: this.makeIdFilter(info.idFields, value) };
return { is: this.makePrismaIdFilter(info.idFields, value) };
}
} else {
const coerced = this.coerce(fieldInfo.type, value);
Expand Down
Loading

0 comments on commit c39f596

Please sign in to comment.