diff --git a/src/api/models/controllers.test.ts b/src/api/models/controllers.test.ts
index 70a5fccdd9a34e1adca3a2894f5783986e47e698..3542658106b2b399aac6074ce76bcf055fcadd00 100644
--- a/src/api/models/controllers.test.ts
+++ b/src/api/models/controllers.test.ts
@@ -12,6 +12,7 @@ import {
find,
get,
getEvents,
+ patch,
restore,
timetravel,
update,
@@ -355,6 +356,205 @@ describe('controllers/models', () => {
});
});
+ describe('#patch', () => {
+ let error;
+ let req;
+ let res;
+ let next;
+
+ beforeEach(() => {
+ error = null;
+ next = jest.fn().mockImplementation((err) => (error = err));
+ req = {
+ params: {},
+ body: {},
+ };
+ res = {};
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('calls next if res.body is already set', async () => {
+ const controller = patch({ mongodb, models, telemetry });
+
+ res.body = {};
+ await controller(req, res, next);
+
+ expect(next).toHaveBeenCalledTimes(1);
+ expect(next).toHaveBeenLastCalledWith();
+ });
+
+ it('returns an error if the model is invalid', async () => {
+ const controller = patch({ mongodb, models, telemetry });
+
+ await controller(req, res, next);
+
+ expect(error).not.toBe(null);
+ expect(error.message).toEqual('Invalid Model');
+ expect(error.status).toEqual(400);
+ });
+
+ it('returns a 422 Unprocessable Entity in case of an update applied on a non created entity', async () => {
+ const controller = patch({ mongodb, models, telemetry });
+
+ req.params.model = 'users';
+ req.params.correlation_id = ObjectId().toString();
+ req.body = {
+ json_patch: [
+ {
+ op: 'replace',
+ path: '/firstname',
+ value: 'John',
+ },
+ ],
+ };
+
+ await controller(req, res, next);
+
+ expect(error).not.toBe(null);
+ expect(error.message).toEqual('Entity must be created first');
+ expect(error.status).toEqual(422);
+ });
+
+ it('returns a validation error in case invalid body schema validation', async () => {
+ const controller = patch({ mongodb, models, telemetry });
+
+ const user = models.factory(mongodb, 'users');
+
+ await user.create({
+ firstname: 'John',
+ });
+
+ req.params.model = 'users';
+ req.params.correlation_id = user.state.user_id.toString();
+ req.body = {
+ firstname: 'John', // Must only accept `json_patch`
+ };
+
+ await controller(req, res, next);
+
+ expect(error).not.toBe(null);
+ expect(error.message).toEqual('Event schema validation error');
+ expect(error.details).toEqual([
+ {
+ dataPath: '',
+ keyword: 'additionalProperties',
+ message: 'should NOT have additional properties',
+ params: { additionalProperty: 'firstname' },
+ schemaPath: '#/additionalProperties',
+ },
+ ]);
+ expect(error.status).toEqual(400);
+ });
+
+ it('returns a generic error otherwise', async () => {
+ const controller = patch({ mongodb, models, telemetry });
+
+ const user = models.factory(mongodb, 'users');
+
+ await user.create({
+ firstname: 'John',
+ });
+
+ req.params.model = 'users';
+ req.params.correlation_id = user.state.user_id.toString();
+ req.body = {
+ json_patch: [
+ {
+ op: 'replace',
+ path: '/firstname',
+ value: 'John',
+ },
+ ],
+ };
+
+ next = jest
+ .fn()
+ .mockImplementationOnce(() => {
+ throw new Error('Oooops');
+ })
+ .mockImplementation((err) => (error = err));
+
+ await controller(req, res, next);
+
+ expect(error).not.toBe(null);
+ expect(error.message).toEqual('Oooops');
+ });
+
+ it('returns the patched entity', async () => {
+ const controller = patch({ mongodb, models, telemetry });
+
+ const user = models.factory(mongodb, 'users');
+
+ await user.create({
+ firstname: 'John',
+ });
+
+ req.params.model = 'users';
+ req.params.correlation_id = user.state.user_id.toString();
+ req.body = {
+ json_patch: [
+ {
+ op: 'replace',
+ path: '/firstname',
+ value: 'Jack',
+ },
+ ],
+ };
+
+ await controller(req, res, next);
+
+ expect(next).toHaveBeenCalledWith();
+
+ expect(res.body).toMatchObject({
+ firstname: 'Jack',
+ version: 1,
+ is_enabled: true,
+ });
+
+ expect(res.body).toHaveProperty('created_at');
+ expect(res.body).toHaveProperty('updated_at');
+ expect(res.body).toHaveProperty('user_id');
+
+ expect(res.body.created_at).not.toEqual(res.body.updated_at);
+ });
+
+ it('returns a 409 Conflict error in case of index violation', async () => {
+ const controller = patch({ mongodb, models, telemetry });
+
+ const alice = models.factory(mongodb, 'users');
+ await alice.create({
+ firstname: 'John',
+ email: 'john@doe.org',
+ });
+
+ const user = models.factory(mongodb, 'users');
+ await user.create({ firstname: 'John' });
+
+ req.params.model = 'users';
+ req.params.correlation_id = user.state.user_id.toString();
+ req.body = {
+ json_patch: [
+ {
+ op: 'replace',
+ path: '/email',
+ value: 'john@doe.org',
+ },
+ ],
+ };
+
+ await controller(req, res, next);
+
+ expect(next).toHaveBeenCalledTimes(1);
+
+ expect(error).not.toBe(null);
+ expect(error.message).toContain('E11000 duplicate key error collection');
+ expect(error.status).toEqual(409);
+ });
+ });
+
describe('#apply', () => {
let error;
let req;
@@ -998,6 +1198,32 @@ describe('controllers/models', () => {
});
});
+ it('returns a list of entities skipping the response validation with with-response-validation header set to false', async () => {
+ const controller = find({ mongodb, models, telemetry });
+
+ const users = [
+ models.factory(mongodb, 'users'),
+ models.factory(mongodb, 'users'),
+ models.factory(mongodb, 'users'),
+ models.factory(mongodb, 'users'),
+ ];
+
+ await Promise.all(
+ users.map((user, index) => user.create({ firstname: `Joe_${index}` })),
+ );
+
+ req.params.model = 'users';
+ req.headers['with-response-validation'] = 'false';
+
+ res.json = jest.fn();
+
+ await controller(req, res, next);
+
+ expect(res.body.length).toBeGreaterThanOrEqual(4);
+ expect(next).toHaveBeenCalledTimes(0);
+ expect(res.json).toHaveBeenCalledWith(res.body);
+ });
+
it('returns a list of entities of max page_size', async () => {
const controller = find({ mongodb, models, telemetry });
diff --git a/src/api/models/controllers.ts b/src/api/models/controllers.ts
index 9f3a166d334f9c3378fc1b82825031b42dcb2f17..12705aa420ecca3e3b352775f0e2c4ff73e08e62 100644
--- a/src/api/models/controllers.ts
+++ b/src/api/models/controllers.ts
@@ -123,6 +123,71 @@ export function update(services) {
};
}
+export function patch(services) {
+ const metric = 'api.model.patch';
+ const { logger, meter } = services.telemetry;
+ meter.counter(metric, meter.meter('api'));
+
+ return async (req: Request, res: Response, next: NextFunction) => {
+ // @ts-ignore
+ if (res.body) {
+ return next();
+ }
+
+ try {
+ meter.add(metric, 1, { state: 'request', model: req.params.model });
+ const model = services.models.factory(
+ services.mongodb,
+ req.params.model,
+ req.params.correlation_id,
+ );
+
+ await model.patch(req.body);
+
+ // @ts-ignore
+ res.body = model.state;
+
+ logger.debug('[api/models#patch] Entity patchd', {
+ model: req.params.model,
+ entity: model.state,
+ });
+
+ meter.add(metric, 1, { state: '200', model: req.params.model });
+ next();
+ } catch (err) {
+ if (err.message === 'Invalid Model') {
+ meter.add(metric, 1, { state: '400', model: req.params.model });
+
+ err.status = 400;
+ return next(err);
+ }
+
+ if (err.message === 'Event schema validation error') {
+ meter.add(metric, 1, { state: '400', model: req.params.model });
+
+ err.status = 400;
+ return next(err);
+ }
+
+ if (err.code === 11000) {
+ meter.add(metric, 1, { state: '409', model: req.params.model });
+
+ err.status = 409;
+ return next(err);
+ }
+
+ if (err.message === 'Entity must be created first') {
+ meter.add(metric, 1, { state: '422', model: req.params.model });
+
+ err.status = 422;
+ return next(err);
+ }
+
+ next(err);
+ }
+ };
+}
+
export function apply(services) {
const metric = 'api.model.apply';
const { meter } = services.telemetry;
diff --git a/src/api/models/index.ts b/src/api/models/index.ts
index 2e6659742e585e2056951b311a125ec882d4c213..0f51b063c4cebce5ce985e93acd70e9402f1127e 100644
--- a/src/api/models/index.ts
+++ b/src/api/models/index.ts
@@ -8,6 +8,7 @@ import {
find,
get,
getEvents,
+ patch,
restore,
timetravel,
update,
@@ -22,6 +23,7 @@ function routes(services) {
.post('/decrypt', decrypt(services))
.get('/:correlation_id', get(services))
.post('/:correlation_id', update(services))
+ .patch('/:correlation_id', patch(services))
.get('/:correlation_id/events', getEvents(services))
.post('/:correlation_id/snapshot', createSnapshot(services))
.get('/:correlation_id/:version', timetravel(services))
diff --git a/src/api/spec/__snapshots__/builder.test.ts.snap b/src/api/spec/__snapshots__/builder.test.ts.snap
index cda544b2c9196be20c5952787576cc0ed864ef78..77f35d8a2e336c15307e0bf2b7c2b60d59c50049 100644
--- a/src/api/spec/__snapshots__/builder.test.ts.snap
+++ b/src/api/spec/__snapshots__/builder.test.ts.snap
@@ -631,6 +631,131 @@ Object {
"Users",
],
},
+ "patch": Object {
+ "description": "Patch an existing User already present in the database.",
+ "operationId": "patchUser",
+ "parameters": Array [
+ Object {
+ "description": "Correlation id",
+ "in": "path",
+ "name": undefined,
+ "required": true,
+ "schema": Object {
+ "description": "Unique ID used as a correlation ID",
+ "example": "5fa962406a651140b5f9f4bf",
+ "type": "string",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "json_patch": Object {
+ "items": Object {
+ "properties": Object {
+ "op": Object {
+ "description": "JSON PATCH Operation",
+ "enum": Array [
+ "add",
+ "remove",
+ "replace",
+ "copy",
+ "move",
+ ],
+ "type": "string",
+ },
+ "path": Object {
+ "description": "JSON PATCH Operation path",
+ "type": "string",
+ },
+ },
+ "required": Array [
+ "op",
+ "path",
+ ],
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "required": Array [
+ "json_patch",
+ ],
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "Entity successfully patched",
+ "links": Object {},
+ },
+ "400": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Invalid Model",
+ "status": 400,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Invalid Model / Event schema validation error",
+ },
+ "409": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Conflict",
+ "status": 409,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Object is in conflict with another",
+ },
+ "422": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Entity must be created first",
+ "status": 422,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Entity must be created first",
+ },
+ "default": Object {
+ "$ref": "#/components/responses/default",
+ },
+ },
+ "security": Array [
+ Object {
+ "apiKey": Array [],
+ },
+ ],
+ "summary": "Patch a User",
+ "tags": Array [
+ "Users",
+ ],
+ },
"post": Object {
"description": "Update an existing User already present in the database.",
"operationId": "updateUser",
@@ -1089,7 +1214,7 @@ Object {
Object {
"description": "Routes available for the model User
-Events available to this model: CREATED, UPDATED, RESTORED, ROLLBACKED.
+Events available to this model: CREATED, UPDATED, PATCHED, RESTORED, ROLLBACKED.
User JSON Schema
@@ -1771,6 +1896,131 @@ Object {
"Users",
],
},
+ "patch": Object {
+ "description": "Patch an existing User already present in the database.",
+ "operationId": "patchUser",
+ "parameters": Array [
+ Object {
+ "description": "Correlation id",
+ "in": "path",
+ "name": undefined,
+ "required": true,
+ "schema": Object {
+ "description": "Unique ID used as a correlation ID",
+ "example": "5fa962406a651140b5f9f4bf",
+ "type": "string",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "json_patch": Object {
+ "items": Object {
+ "properties": Object {
+ "op": Object {
+ "description": "JSON PATCH Operation",
+ "enum": Array [
+ "add",
+ "remove",
+ "replace",
+ "copy",
+ "move",
+ ],
+ "type": "string",
+ },
+ "path": Object {
+ "description": "JSON PATCH Operation path",
+ "type": "string",
+ },
+ },
+ "required": Array [
+ "op",
+ "path",
+ ],
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "required": Array [
+ "json_patch",
+ ],
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "Entity successfully patched",
+ "links": Object {},
+ },
+ "400": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Invalid Model",
+ "status": 400,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Invalid Model / Event schema validation error",
+ },
+ "409": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Conflict",
+ "status": 409,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Object is in conflict with another",
+ },
+ "422": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Entity must be created first",
+ "status": 422,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Entity must be created first",
+ },
+ "default": Object {
+ "$ref": "#/components/responses/default",
+ },
+ },
+ "security": Array [
+ Object {
+ "apiKey": Array [],
+ },
+ ],
+ "summary": "Patch a User",
+ "tags": Array [
+ "Users",
+ ],
+ },
"post": Object {
"description": "Update an existing User already present in the database.",
"operationId": "updateUser",
@@ -2047,6 +2297,20 @@ Object {
},
"description": "Object is in conflict with another",
},
+ "422": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Entity must be created first",
+ "status": 422,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Entity must be created first",
+ },
"default": Object {
"$ref": "#/components/responses/default",
},
@@ -2318,7 +2582,7 @@ Object {
Object {
"description": "Routes available for the model User
-Events available to this model: CREATED, UPDATED, RESTORED, ROLLBACKED, FIRSTNAME_UPDATED.
+Events available to this model: CREATED, UPDATED, PATCHED, RESTORED, ROLLBACKED, FIRSTNAME_UPDATED.
User JSON Schema
@@ -3000,6 +3264,131 @@ Object {
"Users",
],
},
+ "patch": Object {
+ "description": "Patch an existing User already present in the database.",
+ "operationId": "patchUser",
+ "parameters": Array [
+ Object {
+ "description": "Correlation id",
+ "in": "path",
+ "name": undefined,
+ "required": true,
+ "schema": Object {
+ "description": "Unique ID used as a correlation ID",
+ "example": "5fa962406a651140b5f9f4bf",
+ "type": "string",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "json_patch": Object {
+ "items": Object {
+ "properties": Object {
+ "op": Object {
+ "description": "JSON PATCH Operation",
+ "enum": Array [
+ "add",
+ "remove",
+ "replace",
+ "copy",
+ "move",
+ ],
+ "type": "string",
+ },
+ "path": Object {
+ "description": "JSON PATCH Operation path",
+ "type": "string",
+ },
+ },
+ "required": Array [
+ "op",
+ "path",
+ ],
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "required": Array [
+ "json_patch",
+ ],
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "Entity successfully patched",
+ "links": Object {},
+ },
+ "400": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Invalid Model",
+ "status": 400,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Invalid Model / Event schema validation error",
+ },
+ "409": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Conflict",
+ "status": 409,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Object is in conflict with another",
+ },
+ "422": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Entity must be created first",
+ "status": 422,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Entity must be created first",
+ },
+ "default": Object {
+ "$ref": "#/components/responses/default",
+ },
+ },
+ "security": Array [
+ Object {
+ "apiKey": Array [],
+ },
+ ],
+ "summary": "Patch a User",
+ "tags": Array [
+ "Users",
+ ],
+ },
"post": Object {
"description": "Update an existing User already present in the database.",
"operationId": "updateUser",
@@ -3274,6 +3663,20 @@ Object {
},
"description": "Object is in conflict with another",
},
+ "422": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Entity must be created first",
+ "status": 422,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Entity must be created first",
+ },
"default": Object {
"$ref": "#/components/responses/default",
},
@@ -3545,7 +3948,7 @@ Object {
Object {
"description": "Routes available for the model User
-Events available to this model: CREATED, UPDATED, RESTORED, ROLLBACKED, FIRSTNAME_UPDATED.
+Events available to this model: CREATED, UPDATED, PATCHED, RESTORED, ROLLBACKED, FIRSTNAME_UPDATED.
User JSON Schema
@@ -4232,6 +4635,131 @@ Object {
"Users",
],
},
+ "patch": Object {
+ "description": "Patch an existing User already present in the database.",
+ "operationId": "patchUser",
+ "parameters": Array [
+ Object {
+ "description": "Correlation id",
+ "in": "path",
+ "name": undefined,
+ "required": true,
+ "schema": Object {
+ "description": "Unique ID used as a correlation ID",
+ "example": "5fa962406a651140b5f9f4bf",
+ "type": "string",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "json_patch": Object {
+ "items": Object {
+ "properties": Object {
+ "op": Object {
+ "description": "JSON PATCH Operation",
+ "enum": Array [
+ "add",
+ "remove",
+ "replace",
+ "copy",
+ "move",
+ ],
+ "type": "string",
+ },
+ "path": Object {
+ "description": "JSON PATCH Operation path",
+ "type": "string",
+ },
+ },
+ "required": Array [
+ "op",
+ "path",
+ ],
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "required": Array [
+ "json_patch",
+ ],
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "Entity successfully patched",
+ "links": Object {},
+ },
+ "400": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Invalid Model",
+ "status": 400,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Invalid Model / Event schema validation error",
+ },
+ "409": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Conflict",
+ "status": 409,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Object is in conflict with another",
+ },
+ "422": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Entity must be created first",
+ "status": 422,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Entity must be created first",
+ },
+ "default": Object {
+ "$ref": "#/components/responses/default",
+ },
+ },
+ "security": Array [
+ Object {
+ "apiKey": Array [],
+ },
+ ],
+ "summary": "Patch a User",
+ "tags": Array [
+ "Users",
+ ],
+ },
"post": Object {
"description": "Update an existing User already present in the database.",
"operationId": "updateUser",
@@ -4690,7 +5218,7 @@ Object {
Object {
"description": "Routes available for the model User
-Events available to this model: CREATED, UPDATED, RESTORED, ROLLBACKED.
+Events available to this model: CREATED, UPDATED, PATCHED, RESTORED, ROLLBACKED.
User JSON Schema
@@ -5372,6 +5900,131 @@ Object {
"Users",
],
},
+ "patch": Object {
+ "description": "Patch an existing User already present in the database.",
+ "operationId": "patchUser",
+ "parameters": Array [
+ Object {
+ "description": "Correlation id",
+ "in": "path",
+ "name": undefined,
+ "required": true,
+ "schema": Object {
+ "description": "Unique ID used as a correlation ID",
+ "example": "5fa962406a651140b5f9f4bf",
+ "type": "string",
+ },
+ },
+ ],
+ "requestBody": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "properties": Object {
+ "json_patch": Object {
+ "items": Object {
+ "properties": Object {
+ "op": Object {
+ "description": "JSON PATCH Operation",
+ "enum": Array [
+ "add",
+ "remove",
+ "replace",
+ "copy",
+ "move",
+ ],
+ "type": "string",
+ },
+ "path": Object {
+ "description": "JSON PATCH Operation path",
+ "type": "string",
+ },
+ },
+ "required": Array [
+ "op",
+ "path",
+ ],
+ "type": "object",
+ },
+ "type": "array",
+ },
+ },
+ "required": Array [
+ "json_patch",
+ ],
+ "type": "object",
+ },
+ },
+ },
+ },
+ "responses": Object {
+ "200": Object {
+ "content": Object {
+ "application/json": Object {
+ "schema": Object {
+ "$ref": "#/components/schemas/User",
+ },
+ },
+ },
+ "description": "Entity successfully patched",
+ "links": Object {},
+ },
+ "400": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Invalid Model",
+ "status": 400,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Invalid Model / Event schema validation error",
+ },
+ "409": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Conflict",
+ "status": 409,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Object is in conflict with another",
+ },
+ "422": Object {
+ "content": Object {
+ "application/json": Object {
+ "example": Object {
+ "message": "Entity must be created first",
+ "status": 422,
+ },
+ "schema": Object {
+ "$ref": "#/components/schemas/error",
+ },
+ },
+ },
+ "description": "Entity must be created first",
+ },
+ "default": Object {
+ "$ref": "#/components/responses/default",
+ },
+ },
+ "security": Array [
+ Object {
+ "apiKey": Array [],
+ },
+ ],
+ "summary": "Patch a User",
+ "tags": Array [
+ "Users",
+ ],
+ },
"post": Object {
"description": "Update an existing User already present in the database.",
"operationId": "updateUser",
@@ -5835,7 +6488,7 @@ Object {
Object {
"description": "Routes available for the model User
-Events available to this model: CREATED, UPDATED, RESTORED, ROLLBACKED.
+Events available to this model: CREATED, UPDATED, PATCHED, RESTORED, ROLLBACKED.
User JSON Schema
diff --git a/src/api/spec/builder.ts b/src/api/spec/builder.ts
index 0044d5b35aa9def2edd3e2d864642843eedb0b97..edab7d948d3b5a4bc7b76f95591d68b6a163ae7f 100644
--- a/src/api/spec/builder.ts
+++ b/src/api/spec/builder.ts
@@ -47,7 +47,6 @@ export function findLinks(model, models) {
if (k.endsWith('_id') && k !== model.getCorrelationField()) {
const linkedModel = models.getModelByCorrelationField(k);
if (linkedModel !== null) {
- // console.log(44, model.getCollectionName(), k, p);
links[k] = {
path: [
...p.filter(
@@ -127,6 +126,7 @@ ${JSON.stringify(schema.model, null, 2)}
[`/${collection}/{${model.getCorrelationField()}}`]: {
get: getModel(model, links),
post: updateModel(model, links),
+ patch: patchModel(model, links),
},
[`/${collection}/{${model.getCorrelationField()}}/events`]: {
get: getModelEvents(model, links),
@@ -159,7 +159,6 @@ function getEntityName(model, singular = false) {
function buildLinks(links) {
const _links = {};
for (const key in links) {
- // console.log(links[key]);
_links[getEntityName(links[key].model, true)] = {
operationId: eventNametoCamelCase(
`GET_${getEntityName(links[key].model, true)}`,
@@ -169,7 +168,6 @@ function buildLinks(links) {
* @fixme Apply the camelCase only for GraphQL because in the current
* implementation, the link on REST is invalid
*/
- // [key]: `$response.body#/${key}`, // Normal valid way
[key]: `$response.body#/${eventNametoCamelCase(links[key].path)}`, // For GraphQL
},
};
@@ -209,6 +207,7 @@ export function buildAdditionalPaths(model, links) {
c.EVENT_TYPE_UPDATED,
c.EVENT_TYPE_RESTORED,
c.EVENT_TYPE_ROLLBACKED,
+ c.EVENT_TYPE_PATCHED,
].includes(eventType)
) {
const eventTypeLowered = eventType.toLocaleLowerCase();
@@ -272,6 +271,7 @@ export function buildAdditionalPaths(model, links) {
description: 'Invalid Model / Event schema validation error',
},
409: components.responses[409],
+ 422: components.responses[422],
},
}),
};
@@ -541,6 +541,64 @@ export function updateModel(model, links) {
return routeSpec;
}
+export function patchModel(model, links) {
+ const schema = model.getSchema();
+
+ const event = schema.events[c.EVENT_TYPE_PATCHED]['0_0_0'];
+ const entityName = getEntityName(model, true);
+
+ const routeSpec = merge({}, defaultSchema(model), {
+ operationId: eventNametoCamelCase(`patch_${entityName}`),
+ summary: event.title || `Patch a ${entityName}`,
+ description:
+ event.description ||
+ `Patch an existing ${entityName} already present in the database.`,
+ parameters: [
+ {
+ in: 'path',
+ name: model.getCorrelationField(),
+ description: 'Correlation id',
+ schema: c.COMPONENT_CORRELATION_ID,
+ required: true,
+ },
+ ],
+ requestBody: {
+ content: {
+ [MIME_APPLICATION_JSON]: {
+ schema: {
+ type: 'object',
+ required: ['json_patch'],
+ properties: {
+ json_patch: event.properties.json_patch,
+ },
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
+ description: 'Entity successfully patched',
+ content: {
+ [MIME_APPLICATION_JSON]: {
+ schema: {
+ $ref: `#/components/schemas/${entityName}`,
+ },
+ },
+ },
+ links: buildLinks(links),
+ },
+ 400: {
+ ...components.responses[400],
+ description: 'Invalid Model / Event schema validation error',
+ },
+ 409: components.responses[409],
+ 422: components.responses[422],
+ },
+ });
+
+ return routeSpec;
+}
+
export function getModels(model, links) {
const modelConfig = model.getModelConfig();
const originalSchema = model.getOriginalSchema();
@@ -632,7 +690,6 @@ export function getModels(model, links) {
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`GET_${entityName}`),
- // operationId: entityName.toLowerCase(),
summary: `Find ${entityName}`,
description: `Find ${entityName} present in the database.`,
parameters,
@@ -685,7 +742,6 @@ export function getModel(model, links) {
return merge({}, defaultSchema(model), {
operationId: eventNametoCamelCase(`GET_${entityName}`),
- // operationId: entityName.toLowerCase(),
summary: `Get a ${entityName}`,
description: `Get a specific ${entityName} uniquely identified
by its ${model.getCorrelationField()}.`,
@@ -721,7 +777,6 @@ export function getModelAtVersion(model, links) {
const entityName = getEntityName(model, true);
return merge({}, defaultSchema(model), {
- // operationId: eventNametoCamelCase(`GET_${entityName}_VERSION`),
operationId: eventNametoCamelCase(`${entityName}_AT_VERSION`),
summary: `Get ${entityName} at version`,
description: `Get a specific version for a ${entityName}.`,
diff --git a/src/constants/events.ts b/src/constants/events.ts
index e174c63eebad82034e6d7d6177be74ffa8a2012e..ab03e54f674449580fea1032c23a980504428720 100644
--- a/src/constants/events.ts
+++ b/src/constants/events.ts
@@ -2,3 +2,4 @@ export const EVENT_TYPE_CREATED: string = 'CREATED';
export const EVENT_TYPE_UPDATED: string = 'UPDATED';
export const EVENT_TYPE_RESTORED: string = 'RESTORED';
export const EVENT_TYPE_ROLLBACKED: string = 'ROLLBACKED';
+export const EVENT_TYPE_PATCHED: string = 'PATCHED';
diff --git a/src/models/Generic.ts b/src/models/Generic.ts
index cd50ee0b8ad023334e1e824eca95fe3c104f04ce..2bd6c3093a02ab4ad7a5031682163f92b6ae928e 100644
--- a/src/models/Generic.ts
+++ b/src/models/Generic.ts
@@ -212,6 +212,10 @@ export default (
return this.apply(c.EVENT_TYPE_UPDATED, data);
}
+ public patch(data: any): Promise {
+ return this.apply(c.EVENT_TYPE_PATCHED, data);
+ }
+
public async restore(version: number): Promise {
const state = await this.getStateAtVersion(version);
diff --git a/src/models/schema.ts b/src/models/schema.ts
index 55eb6607adef6478702c454aaaaf633c18e535d1..6b17c2ace50c17ee964f7d5f7d1c43766d0d4756 100644
--- a/src/models/schema.ts
+++ b/src/models/schema.ts
@@ -69,6 +69,16 @@ export default (modelConfig: ModelConfig) => {
},
},
},
+ [c.EVENT_TYPE_PATCHED]: {
+ '0_0_0': {
+ type: 'object',
+ additionalProperties: false,
+ required: ['json_patch'],
+ properties: {
+ json_patch: c.COMPONENT_JSON_PATCH,
+ },
+ },
+ },
[c.EVENT_TYPE_RESTORED]: {
'0_0_0': {
type: 'object',
diff --git a/src/sdk/Datastore.test.ts b/src/sdk/Datastore.test.ts
index 7d6f08578e5277c732b47cb161681ce12df01246..4620c9ce0136f69b097ab58ea1aaee57174230bb 100644
--- a/src/sdk/Datastore.test.ts
+++ b/src/sdk/Datastore.test.ts
@@ -2,6 +2,7 @@ import util from 'util';
import Datastore, {
ERROR_MISSING_CORRELATION_ID,
+ ERROR_MISSING_JSON_PATCH,
ERROR_MISSING_MODEL_NAME,
} from './Datastore';
@@ -530,6 +531,60 @@ describe('sdk/Datastore', () => {
});
});
+ describe('#patch', () => {
+ it('calls the patch model route', async () => {
+ await client.patch('users', 'user_correlation_id', [
+ {
+ op: 'replace',
+ path: '/firstname',
+ value: 'Jack',
+ },
+ ]);
+
+ expect(client.axios.request).toHaveBeenLastCalledWith({
+ method: 'patch',
+ url: '/api/users/user_correlation_id',
+ data: {
+ json_patch: [
+ {
+ op: 'replace',
+ path: '/firstname',
+ value: 'Jack',
+ },
+ ],
+ },
+ });
+ });
+
+ it('throws an error if no correlation ID is provided', async () => {
+ let error;
+ try {
+ await client.patch('users', undefined, [
+ {
+ op: 'replace',
+ path: '/firstname',
+ value: 'Jack',
+ },
+ ]);
+ } catch (err) {
+ error = err;
+ }
+
+ expect(error).toEqual(ERROR_MISSING_CORRELATION_ID);
+ });
+
+ it('throws an error if no JSON Patch is provided', async () => {
+ let error;
+ try {
+ await client.patch('users', 'user_id');
+ } catch (err) {
+ error = err;
+ }
+
+ expect(error).toEqual(ERROR_MISSING_JSON_PATCH);
+ });
+ });
+
describe('#get', () => {
it('calls the get model route', async () => {
await client.get('users', 'user_correlation_id');
diff --git a/src/sdk/Datastore.ts b/src/sdk/Datastore.ts
index 44eff98dc4979aec73303684123aca0294d85cc2..912f2c63cf423c93f8c9ac85ce1f8c1797f67c05 100644
--- a/src/sdk/Datastore.ts
+++ b/src/sdk/Datastore.ts
@@ -8,6 +8,7 @@ import type { DatastoreImportFixture, ModelConfig } from '../../typings';
export const ERROR_MISSING_MODEL_NAME = new Error('Missing Model name');
export const ERROR_MISSING_CORRELATION_ID = new Error('Missing Correlation ID');
+export const ERROR_MISSING_JSON_PATCH = new Error('Missing JSON Patch');
function mergeWithArrays(objValue, srcValue) {
if (Array.isArray(objValue)) {
@@ -376,6 +377,25 @@ ${type} ${operationName}($token: String!${
});
}
+ patch(
+ modelName: string,
+ correlationId: string,
+ jsonPatch: object[],
+ ): Promise {
+ this._checkCorrelationIdExistence(correlationId);
+ if (!jsonPatch) {
+ throw ERROR_MISSING_JSON_PATCH;
+ }
+
+ return this.request({
+ method: 'patch',
+ url: this.getPath(modelName, correlationId),
+ data: {
+ json_patch: jsonPatch,
+ },
+ });
+ }
+
get(modelName: string, correlationId: string): Promise {
this._checkCorrelationIdExistence(correlationId);