Skip to content

Commit 3d5022e

Browse files
committed
add ability to define any unique ID to be used as the ID within a REST API
this makes it possible to use a natural key even while also using surrogate primary keys
1 parent 6d11737 commit 3d5022e

File tree

2 files changed

+32
-6
lines changed

2 files changed

+32
-6
lines changed

packages/runtime/src/cross/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export function zip<T1, T2>(x: Enumerable<T1>, y: Enumerable<T2>): Array<[T1, T2
5151
*/
5252
export function getIdFields(modelMeta: ModelMeta, model: string, throwIfNotFound = false) {
5353
const uniqueConstraints = modelMeta.models[lowerCaseFirst(model)]?.uniqueConstraints ?? {};
54-
5554
const entries = Object.values(uniqueConstraints);
5655
if (entries.length === 0) {
5756
if (throwIfNotFound) {

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
PrismaErrorCode,
88
clone,
99
enumerate,
10+
requireField,
1011
getIdFields,
1112
isPrismaClientKnownRequestError,
1213
} from '@zenstackhq/runtime';
@@ -52,6 +53,8 @@ export type Options = {
5253
urlSegmentCharset?: string;
5354

5455
modelNameMapping?: Record<string, string>;
56+
57+
externalIdMapping: Record<string, string>;
5558
};
5659

5760
type RelationshipInfo = {
@@ -238,6 +241,7 @@ class RequestHandler extends APIHandlerBase {
238241
private urlPatternMap: Record<UrlPatterns, UrlPattern>;
239242
private modelNameMapping: Record<string, string>;
240243
private reverseModelNameMapping: Record<string, string>;
244+
private externalIdMapping: Record<string, string>;
241245

242246
constructor(private readonly options: Options) {
243247
super();
@@ -251,6 +255,12 @@ class RequestHandler extends APIHandlerBase {
251255
this.reverseModelNameMapping = Object.fromEntries(
252256
Object.entries(this.modelNameMapping).map(([k, v]) => [v, k])
253257
);
258+
259+
this.externalIdMapping = options.externalIdMapping ?? {};
260+
this.externalIdMapping = Object.fromEntries(
261+
Object.entries(this.externalIdMapping).map(([k, v]) => [lowerCaseFirst(k), v])
262+
);
263+
254264
this.urlPatternMap = this.buildUrlPatternMap(segmentCharset);
255265
}
256266

@@ -1166,11 +1176,28 @@ class RequestHandler extends APIHandlerBase {
11661176
}
11671177

11681178
//#region utilities
1179+
private getIdFields(modelMeta: ModelMeta, model: string): FieldInfo[] {
1180+
const modelLower = lowerCaseFirst(model);
1181+
if (!(modelLower in this.externalIdMapping)) {
1182+
return getIdFields(modelMeta, model);
1183+
}
1184+
1185+
const metaData = modelMeta.models[modelLower] ?? {};
1186+
const externalIdName = this.externalIdMapping[modelLower];
1187+
const uniqueConstraints = metaData.uniqueConstraints ?? {};
1188+
for (const [name, constraint] of Object.entries(uniqueConstraints)) {
1189+
if (name === externalIdName) {
1190+
return constraint.fields.map((f) => requireField(modelMeta, model, f));
1191+
}
1192+
}
1193+
1194+
throw new Error(`Model ${model} does not have unique key ${externalIdName}`);
1195+
}
11691196

11701197
private buildTypeMap(logger: LoggerConfig | undefined, modelMeta: ModelMeta): void {
11711198
this.typeMap = {};
11721199
for (const [model, { fields }] of Object.entries(modelMeta.models)) {
1173-
const idFields = getIdFields(modelMeta, model);
1200+
const idFields = this.getIdFields(modelMeta, model);
11741201
if (idFields.length === 0) {
11751202
logWarning(logger, `Not including model ${model} in the API because it has no ID field`);
11761203
continue;
@@ -1186,7 +1213,7 @@ class RequestHandler extends APIHandlerBase {
11861213
if (!fieldInfo.isDataModel) {
11871214
continue;
11881215
}
1189-
const fieldTypeIdFields = getIdFields(modelMeta, fieldInfo.type);
1216+
const fieldTypeIdFields = this.getIdFields(modelMeta, fieldInfo.type);
11901217
if (fieldTypeIdFields.length === 0) {
11911218
logWarning(
11921219
logger,
@@ -1214,7 +1241,7 @@ class RequestHandler extends APIHandlerBase {
12141241
const linkers: Record<string, Linker<any>> = {};
12151242

12161243
for (const model of Object.keys(modelMeta.models)) {
1217-
const ids = getIdFields(modelMeta, model);
1244+
const ids = this.getIdFields(modelMeta, model);
12181245
const mappedModel = this.mapModelName(model);
12191246

12201247
if (ids.length < 1) {
@@ -1266,7 +1293,7 @@ class RequestHandler extends APIHandlerBase {
12661293
if (!fieldSerializer) {
12671294
continue;
12681295
}
1269-
const fieldIds = getIdFields(modelMeta, fieldMeta.type);
1296+
const fieldIds = this.getIdFields(modelMeta, fieldMeta.type);
12701297
if (fieldIds.length > 0) {
12711298
const mappedModel = this.mapModelName(model);
12721299

@@ -1306,7 +1333,7 @@ class RequestHandler extends APIHandlerBase {
13061333
if (!data) {
13071334
return undefined;
13081335
}
1309-
const ids = getIdFields(modelMeta, model);
1336+
const ids = this.getIdFields(modelMeta, model);
13101337
if (ids.length === 0) {
13111338
return undefined;
13121339
} else {

0 commit comments

Comments
 (0)