## Summary
- Replace the dynamic `RestApiMetadataController` (which parsed
`/rest/metadata/*path` and proxied to internal GraphQL) with two
dedicated controllers: `ObjectMetadataController` and
`FieldMetadataController`.
- Drop the GraphQL hop: reads hit Postgres directly via TypeORM
repositories; writes call the existing
`{create,update,delete}One{Object,Field}` service methods.
- Introduce a new clean response shape behind a workspace feature flag
(`IS_REST_METADATA_API_NEW_FORMAT_DIRECT`) — see grace period below.
- Update the OpenAPI spec so the REST playground reflects the (default)
legacy shape during the grace period.
## Why
The legacy metadata controller was over-complex: it routed every method
through a path parser, a set of GraphQL query-builder factories, an
internal GraphQL call, and a
`cleanGraphQLResponse` post-processor. Operation names from GraphQL
(`createOneObject`, `updateOneField`, …) leaked straight into REST
responses. The internal-GraphQL hop also gave us
nothing on metadata reads — pagination, filtering, and serialization all
happen against the same Postgres tables either way.
## Feature flag & grace period
`IS_REST_METADATA_API_NEW_FORMAT_DIRECT` (workspace-scoped):
- **Existing workspaces:** flag absent → resolves to `false` → **legacy
response shape** (no behavior change).
- **Newly created workspaces:** flag seeded to `true` via
`DEFAULT_FEATURE_FLAGS` → **new response shape** from day one.
- **Toggle:** support-assisted (no frontend); customers contact us to
opt into the new shape early.
- **Removal:** the flag, the legacy adapter utils
(`to-legacy-{object,field}-metadata-response.util.ts`), and the
parametrized test wrapper get deleted after the grace window. New shape
becomes the only shape; OpenAPI flips to new shape; POST loses the
conditional and reverts to a declarative response.
## Response shapes
| Operation | Legacy (flag OFF, default for existing) | New (flag ON) |
|-----------|-----------------------------------------|---------------|
| `GET /rest/metadata/objects` | `{ data: { objects: [...] }, pageInfo,
totalCount }` | `{ data: [...], pageInfo, totalCount }` |
| `GET /rest/metadata/objects/:id` | `{ data: { object: {...} } }` | `{
... }` |
| `POST /rest/metadata/objects` | `201 { data: { createOneObject: {...}
} }` | `201 { ... }` |
| `PATCH/PUT /rest/metadata/objects/:id` | `{ data: { updateOneObject:
{...} } }` | `{ ... }` |
| `DELETE /rest/metadata/objects/:id` | `{ data: { deleteOneObject: {
... } } }` | `{ ... }` |
Same matrix for `/rest/metadata/fields`. Cursor params
(`starting_after`, `ending_before`, `limit`) and `totalCount` are
preserved across both shapes. POST returns `201` in both (old
controller already did — the doc on main saying `200` was wrong).
## Implementation notes
- Reads go straight to Postgres with TypeORM cursor pagination
(`paginateByIdCursor` util, mutually-exclusive `starting_after` /
`ending_before`). No cache on this path — caching +
filterable pagination didn't combine cleanly.
- Object endpoints inline `fields[]` via a single follow-up `WHERE
objectMetadataId IN (...)` query.
- Controllers read the flag via `FeatureFlagService.isFeatureEnabled`
and conditionally pass the result through a legacy-shape adapter util
before returning.
- Per-domain REST exception filters
(`{Object,Field}MetadataRestApiExceptionFilter`); the `exceptionCode →
httpStatus` switch is extracted to a util so it can be merged with the
existing GraphQL handler later.
- New controllers live inside the metadata domain modules
(`metadata-modules/{object,field}-metadata/controllers/`) to match
existing precedent (view-field, view, page-layout, …).
- Removes: `RestApiMetadataController`, `RestApiMetadataService`,
`metadata/query-builder/`, `clean-graphql-response.utils.ts`.
- Integration tests are parametrized over both flag values via
`describe.each` — both shapes are asserted in CI.
- OpenAPI fixes inherited from the migration (kept as-is): documents
flat `fields: [...]` rather than the obsolete `{edges:{node:[...]}}`
wrapping; always emits `totalCount`; POST
status `201`. These match what customers actually receive on both
shapes.
Note: Next goal is to implement something similar for graphql and remove
nestjs-query dependency for those 2 entities, then generalise it.
Note2: We have the same issue with Core Rest API such as
```json
{
"data": {
"createCompany": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"createdAt": "2026-05-07T12:14:52.769Z",
"updatedAt": "2026-05-07T12:14:52.769Z",
"deletedAt": "2026-05-07T12:14:52.769Z",
...
```
with "createCompany" here which is odd compared to REST standards (FYI
@etiennejouan @charlesBochet)
## Before (Without feature flag)
<img width="1346" height="712" alt="Screenshot 2026-05-12 at 20 50 38"
src="https://github.com/user-attachments/assets/316ce225-1045-4aac-97a9-60fd537eb1ec"
/>
<img width="1378" height="729" alt="Screenshot 2026-05-12 at 20 52 24"
src="https://github.com/user-attachments/assets/a621ab6f-e4f8-44d5-817c-1efd25d33c30"
/>
## After (With feature flag)
<img width="1376" height="728" alt="Screenshot 2026-05-12 at 20 50 46"
src="https://github.com/user-attachments/assets/2424d9c5-e4ed-497c-8e5c-6b54d78675e4"
/>
<img width="1375" height="727" alt="Screenshot 2026-05-12 at 20 51 47"
src="https://github.com/user-attachments/assets/101d957f-38ed-45d9-ab7b-f4f4eb983397"
/>
---------
Co-authored-by: prastoin <paul@twenty.com>
499 lines
15 KiB
TypeScript
499 lines
15 KiB
TypeScript
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
|
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
|
import {
|
|
cleanupTestObject,
|
|
createTestObjectViaGraphql,
|
|
extractMetadataItemPayload,
|
|
extractMetadataListPayload,
|
|
NON_EXISTENT_UUID,
|
|
uniqueSuffix,
|
|
} from 'test/integration/rest/utils/metadata-rest-api.util';
|
|
import {
|
|
assertRestApiErrorNotFoundResponse,
|
|
assertRestApiErrorResponse,
|
|
assertRestApiSuccessfulResponse,
|
|
} from 'test/integration/rest/utils/rest-test-assertions.util';
|
|
import { FeatureFlagKey } from 'twenty-shared/types';
|
|
|
|
type ObjectShape = { id: string; fields: unknown[]; labelSingular?: string };
|
|
|
|
describe.each([
|
|
['new format', true],
|
|
['legacy format', false],
|
|
] as const)('Object Metadata REST API (%s)', (_shapeLabel, isNewFormat) => {
|
|
beforeAll(async () => {
|
|
await updateFeatureFlag({
|
|
featureFlag: FeatureFlagKey.IS_REST_METADATA_API_NEW_FORMAT_DIRECT,
|
|
value: isNewFormat,
|
|
expectToFail: false,
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await updateFeatureFlag({
|
|
featureFlag: FeatureFlagKey.IS_REST_METADATA_API_NEW_FORMAT_DIRECT,
|
|
value: false,
|
|
expectToFail: false,
|
|
});
|
|
});
|
|
|
|
describe('GET /metadata/objects', () => {
|
|
const seededIds: string[] = [];
|
|
|
|
beforeAll(async () => {
|
|
for (let i = 0; i < 3; i++) {
|
|
const { id } = await createTestObjectViaGraphql();
|
|
|
|
seededIds.push(id);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await Promise.all(seededIds.map(cleanupTestObject));
|
|
seededIds.length = 0;
|
|
});
|
|
|
|
it('returns the expected envelope shape', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: '/metadata/objects',
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(response);
|
|
if (isNewFormat) {
|
|
expect(response.body).not.toHaveProperty('data.objects');
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
} else {
|
|
expect(Array.isArray(response.body.data?.objects)).toBe(true);
|
|
}
|
|
expect(response.body).toHaveProperty('pageInfo.hasNextPage');
|
|
expect(response.body).toHaveProperty('pageInfo.startCursor');
|
|
expect(response.body).toHaveProperty('pageInfo.endCursor');
|
|
expect(typeof response.body.totalCount).toBe('number');
|
|
expect(response.body.totalCount).toBeGreaterThanOrEqual(seededIds.length);
|
|
});
|
|
|
|
it('inlines fields[] on each object', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: '/metadata/objects?limit=5',
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(response);
|
|
const { items } = extractMetadataListPayload<ObjectShape>(
|
|
response.body,
|
|
'objects',
|
|
);
|
|
|
|
for (const object of items) {
|
|
expect(Array.isArray(object.fields)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('respects limit and surfaces hasNextPage', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: '/metadata/objects?limit=1',
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(response);
|
|
const { items, pageInfo } = extractMetadataListPayload<ObjectShape>(
|
|
response.body,
|
|
'objects',
|
|
);
|
|
|
|
expect(items.length).toBe(1);
|
|
expect(pageInfo.hasNextPage).toBe(true);
|
|
expect(pageInfo.startCursor).toBe(items[0].id);
|
|
expect(pageInfo.endCursor).toBe(items[0].id);
|
|
});
|
|
|
|
it('paginates forward with starting_after without overlap', async () => {
|
|
const firstPage = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: '/metadata/objects?limit=2',
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(firstPage);
|
|
const firstPayload = extractMetadataListPayload<ObjectShape>(
|
|
firstPage.body,
|
|
'objects',
|
|
);
|
|
|
|
const secondPage = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects?limit=2&starting_after=${firstPayload.pageInfo.endCursor}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(secondPage);
|
|
const secondPayload = extractMetadataListPayload<ObjectShape>(
|
|
secondPage.body,
|
|
'objects',
|
|
);
|
|
|
|
const firstIds = firstPayload.items.map((o) => o.id);
|
|
const secondIds = secondPayload.items.map((o) => o.id);
|
|
|
|
expect(firstIds.some((id) => secondIds.includes(id))).toBe(false);
|
|
});
|
|
|
|
it('paginates backward with ending_before', async () => {
|
|
const firstPage = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: '/metadata/objects?limit=2',
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
const firstPayload = extractMetadataListPayload<ObjectShape>(
|
|
firstPage.body,
|
|
'objects',
|
|
);
|
|
|
|
const secondPage = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects?limit=2&starting_after=${firstPayload.pageInfo.endCursor}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
const secondPayload = extractMetadataListPayload<ObjectShape>(
|
|
secondPage.body,
|
|
'objects',
|
|
);
|
|
|
|
const backPage = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects?limit=2&ending_before=${secondPayload.pageInfo.startCursor}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(backPage);
|
|
const backPayload = extractMetadataListPayload<ObjectShape>(
|
|
backPage.body,
|
|
'objects',
|
|
);
|
|
|
|
const firstIds = firstPayload.items.map((o) => o.id);
|
|
const backIds = backPayload.items.map((o) => o.id);
|
|
|
|
expect(backIds.every((id) => firstIds.includes(id))).toBe(true);
|
|
});
|
|
|
|
it('rejects combining starting_after and ending_before with 400', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects?starting_after=${NON_EXISTENT_UUID}&ending_before=${NON_EXISTENT_UUID}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorResponse(response, 400);
|
|
});
|
|
|
|
it('keeps totalCount stable across pages', async () => {
|
|
const firstPage = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: '/metadata/objects?limit=2',
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
const firstPayload = extractMetadataListPayload<ObjectShape>(
|
|
firstPage.body,
|
|
'objects',
|
|
);
|
|
|
|
const secondPage = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects?limit=2&starting_after=${firstPayload.pageInfo.endCursor}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(firstPage);
|
|
assertRestApiSuccessfulResponse(secondPage);
|
|
expect(firstPage.body.totalCount).toBe(secondPage.body.totalCount);
|
|
});
|
|
|
|
it('reports hasNextPage=false when the page covers all results', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: '/metadata/objects?limit=200',
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(response);
|
|
const { items, pageInfo } = extractMetadataListPayload<ObjectShape>(
|
|
response.body,
|
|
'objects',
|
|
);
|
|
|
|
expect(items.length).toBe(response.body.totalCount);
|
|
expect(pageInfo.hasNextPage).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('GET /metadata/objects/:id', () => {
|
|
let testObjectId: string;
|
|
|
|
beforeAll(async () => {
|
|
const { id } = await createTestObjectViaGraphql();
|
|
|
|
testObjectId = id;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await cleanupTestObject(testObjectId);
|
|
});
|
|
|
|
it('returns the object with fields[] populated', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects/${testObjectId}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(response);
|
|
const object = extractMetadataItemPayload<ObjectShape>(
|
|
response.body,
|
|
'object',
|
|
);
|
|
|
|
expect(object.id).toBe(testObjectId);
|
|
expect(Array.isArray(object.fields)).toBe(true);
|
|
expect(object.fields.length).toBeGreaterThan(0);
|
|
if (isNewFormat) {
|
|
expect(response.body).not.toHaveProperty('data.object');
|
|
} else {
|
|
expect(response.body).toHaveProperty('data.object');
|
|
}
|
|
});
|
|
|
|
it('returns 400 on a malformed UUID', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects/not-a-uuid`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorResponse(response, 400);
|
|
});
|
|
|
|
it('returns 404 for an unknown id', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects/${NON_EXISTENT_UUID}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorNotFoundResponse(response);
|
|
});
|
|
});
|
|
|
|
describe('POST /metadata/objects', () => {
|
|
it('creates an object and returns 201 with default fields[]', async () => {
|
|
const suffix = uniqueSuffix();
|
|
const input = {
|
|
nameSingular: `postObj${suffix}`,
|
|
namePlural: `postObj${suffix}s`,
|
|
labelSingular: `Post Obj ${suffix}`,
|
|
labelPlural: `Post Objs ${suffix}`,
|
|
icon: 'IconTestPipe',
|
|
isLabelSyncedWithName: false,
|
|
};
|
|
|
|
const response = await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: '/metadata/objects',
|
|
body: input,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
const object = extractMetadataItemPayload<ObjectShape>(
|
|
response.body,
|
|
'createOneObject',
|
|
);
|
|
|
|
try {
|
|
assertRestApiSuccessfulResponse(response, 201);
|
|
expect(object.id).toBeDefined();
|
|
expect(
|
|
(object as ObjectShape & { nameSingular: string }).nameSingular,
|
|
).toBe(input.nameSingular);
|
|
expect(Array.isArray(object.fields)).toBe(true);
|
|
expect(object.fields.length).toBeGreaterThan(0);
|
|
if (isNewFormat) {
|
|
expect(response.body).not.toHaveProperty('data.createOneObject');
|
|
} else {
|
|
expect(response.body).toHaveProperty('data.createOneObject');
|
|
}
|
|
} finally {
|
|
if (object.id) {
|
|
await cleanupTestObject(object.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('returns 400 on duplicate nameSingular', async () => {
|
|
const { id, input } = await createTestObjectViaGraphql();
|
|
|
|
try {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: '/metadata/objects',
|
|
body: input,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorResponse(response, 400);
|
|
} finally {
|
|
await cleanupTestObject(id);
|
|
}
|
|
});
|
|
|
|
it('returns 400 on invalid input', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'post',
|
|
path: '/metadata/objects',
|
|
body: { nameSingular: '' },
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorResponse(response, 400);
|
|
});
|
|
});
|
|
|
|
describe('PATCH /metadata/objects/:id', () => {
|
|
let testObjectId: string;
|
|
|
|
beforeEach(async () => {
|
|
const { id } = await createTestObjectViaGraphql();
|
|
|
|
testObjectId = id;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTestObject(testObjectId);
|
|
});
|
|
|
|
it('updates and returns the object with fields[]', async () => {
|
|
const newLabel = `Updated ${uniqueSuffix()}`;
|
|
|
|
const response = await makeRestAPIRequest({
|
|
method: 'patch',
|
|
path: `/metadata/objects/${testObjectId}`,
|
|
body: { labelSingular: newLabel },
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(response);
|
|
const object = extractMetadataItemPayload<ObjectShape>(
|
|
response.body,
|
|
'updateOneObject',
|
|
);
|
|
|
|
expect(object.id).toBe(testObjectId);
|
|
expect(object.labelSingular).toBe(newLabel);
|
|
expect(Array.isArray(object.fields)).toBe(true);
|
|
if (isNewFormat) {
|
|
expect(response.body).not.toHaveProperty('data.updateOneObject');
|
|
} else {
|
|
expect(response.body).toHaveProperty('data.updateOneObject');
|
|
}
|
|
});
|
|
|
|
it('returns 404 for an unknown id', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'patch',
|
|
path: `/metadata/objects/${NON_EXISTENT_UUID}`,
|
|
body: { labelSingular: 'Whatever' },
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorNotFoundResponse(response);
|
|
});
|
|
});
|
|
|
|
describe('PUT /metadata/objects/:id', () => {
|
|
it('behaves equivalently to PATCH', async () => {
|
|
const { id } = await createTestObjectViaGraphql();
|
|
|
|
try {
|
|
const newLabel = `PutUpdate ${uniqueSuffix()}`;
|
|
|
|
const response = await makeRestAPIRequest({
|
|
method: 'put',
|
|
path: `/metadata/objects/${id}`,
|
|
body: { labelSingular: newLabel },
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(response);
|
|
const object = extractMetadataItemPayload<ObjectShape>(
|
|
response.body,
|
|
'updateOneObject',
|
|
);
|
|
|
|
expect(object.id).toBe(id);
|
|
expect(object.labelSingular).toBe(newLabel);
|
|
expect(Array.isArray(object.fields)).toBe(true);
|
|
} finally {
|
|
await cleanupTestObject(id);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('DELETE /metadata/objects/:id', () => {
|
|
it('deletes the object and returns the deleted resource', async () => {
|
|
const { id } = await createTestObjectViaGraphql();
|
|
|
|
const patchResponse = await makeRestAPIRequest({
|
|
method: 'patch',
|
|
path: `/metadata/objects/${id}`,
|
|
body: { isActive: false },
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(patchResponse);
|
|
|
|
const deleteResponse = await makeRestAPIRequest({
|
|
method: 'delete',
|
|
path: `/metadata/objects/${id}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiSuccessfulResponse(deleteResponse);
|
|
const deleted = extractMetadataItemPayload<{ id: string }>(
|
|
deleteResponse.body,
|
|
'deleteOneObject',
|
|
);
|
|
|
|
expect(deleted.id).toBe(id);
|
|
if (isNewFormat) {
|
|
expect(deleteResponse.body).not.toHaveProperty('data.deleteOneObject');
|
|
} else {
|
|
expect(deleteResponse.body).toHaveProperty('data.deleteOneObject');
|
|
}
|
|
|
|
const getResponse = await makeRestAPIRequest({
|
|
method: 'get',
|
|
path: `/metadata/objects/${id}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorNotFoundResponse(getResponse);
|
|
});
|
|
|
|
it('returns 404 for an unknown id', async () => {
|
|
const response = await makeRestAPIRequest({
|
|
method: 'delete',
|
|
path: `/metadata/objects/${NON_EXISTENT_UUID}`,
|
|
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
|
});
|
|
|
|
assertRestApiErrorNotFoundResponse(response);
|
|
});
|
|
});
|
|
});
|