cap-domain 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +2 -0
- package/README.md +239 -0
- package/cds-plugin.js +5 -0
- package/index.js +33 -0
- package/package.json +25 -0
- package/src/FieldControls.js +356 -0
- package/src/Handlers.js +273 -0
- package/src/ServiceParser.js +56 -0
- package/src/SymbolHelper.js +125 -0
- package/src/Utils.js +61 -0
- package/src/cdsPluginImpl.js +22 -0
- package/src/defaultAnnotationValues.js +44 -0
- package/tests/.cdsrc.json +1 -0
- package/tests/.vscode/extensions.json +18 -0
- package/tests/.vscode/launch.json +18 -0
- package/tests/.vscode/settings.json +11 -0
- package/tests/.vscode/tasks.json +25 -0
- package/tests/_i18n/i18n.properties +5 -0
- package/tests/db/data/my.bookshop-Authors.csv +3 -0
- package/tests/db/data/my.bookshop-Books.csv +3 -0
- package/tests/db/data/my.bookshop-Details.csv +3 -0
- package/tests/db/data/my.bookshop-Request.csv +3 -0
- package/tests/db/data-model.cds +37 -0
- package/tests/eslint.config.js +6 -0
- package/tests/jest.config.js +9 -0
- package/tests/package.json +35 -0
- package/tests/srv/@FCDefinitions/Authors.js +24 -0
- package/tests/srv/@FCDefinitions/Books.js +23 -0
- package/tests/srv/@FCDefinitions/Details.js +16 -0
- package/tests/srv/@FCDefinitions/Request.js +22 -0
- package/tests/srv/CatalogService-annotations.cds +44 -0
- package/tests/srv/CatalogService.cds +46 -0
- package/tests/srv/CatalogService.js +21 -0
- package/tests/srv/_i18n/i18n_de.properties +6 -0
- package/tests/srv/_i18n/i18n_en.properties +6 -0
- package/tests/srv/cat-fiori-service.cds +7 -0
- package/tests/test/UsersApi.js +97 -0
- package/tests/test/authors.test.js +26 -0
- package/tests/test/books.test.js +97 -0
- package/tests/test/request.test.js +39 -0
- package/tests/test/setup.js +54 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
namespace my.bookshop;
|
|
2
|
+
|
|
3
|
+
entity Books {
|
|
4
|
+
key ID : UUID;
|
|
5
|
+
title : String;
|
|
6
|
+
enableDetails : Boolean;
|
|
7
|
+
description : String;
|
|
8
|
+
stock : Integer;
|
|
9
|
+
reorderPoint : Integer;
|
|
10
|
+
releaseDate : Date;
|
|
11
|
+
previewDate : Date;
|
|
12
|
+
statusCode : String;
|
|
13
|
+
price : Integer;
|
|
14
|
+
Author : Association to Authors;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
entity Authors {
|
|
18
|
+
key ID : UUID;
|
|
19
|
+
name : String;
|
|
20
|
+
Books : Composition of many Books
|
|
21
|
+
on Books.Author = $self;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
entity Request {
|
|
26
|
+
key ID : UUID;
|
|
27
|
+
title : String;
|
|
28
|
+
archived : Boolean;
|
|
29
|
+
internalNotes : String;
|
|
30
|
+
note : String;
|
|
31
|
+
Detail : Composition of one Details;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
entity Details {
|
|
35
|
+
key ID : UUID;
|
|
36
|
+
title : String;
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cap-domain-showcase",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple CAP project.",
|
|
5
|
+
"repository": "<Add your repository here>",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"private": true,
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@sap/cds": "^10.1.1",
|
|
10
|
+
"cap-domain": "file:..",
|
|
11
|
+
"express": "^4"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@cap-js/cds-test": "^1.0.2",
|
|
15
|
+
"@cap-js/sqlite": "^3.1.1",
|
|
16
|
+
"@sap/eslint-plugin-cds": "^4.2.5",
|
|
17
|
+
"axios": "^1.8.2",
|
|
18
|
+
"eslint": "^9",
|
|
19
|
+
"jest": "^30.0.0"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "cds-serve",
|
|
23
|
+
"test": "jest --silent=true"
|
|
24
|
+
},
|
|
25
|
+
"cds": {
|
|
26
|
+
"enable:capdomain:plugin": true,
|
|
27
|
+
"requires": {},
|
|
28
|
+
"log": {
|
|
29
|
+
"levels": {
|
|
30
|
+
"sql": "debug",
|
|
31
|
+
"cds": "debug"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { fieldControlDictionary } = require('cap-domain');
|
|
2
|
+
|
|
3
|
+
const fieldControlConfigurations = {
|
|
4
|
+
name: {
|
|
5
|
+
fc: () => fieldControlDictionary.Mandatory,
|
|
6
|
+
validator(value, { i18n }) {
|
|
7
|
+
if (!value || value.length < 10) {
|
|
8
|
+
return i18n.getText(
|
|
9
|
+
'book.validation.title.minLength'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// addOnHAndler(fieldControlConfigurations, (req, next) => {
|
|
17
|
+
// this.srvFn();
|
|
18
|
+
// })
|
|
19
|
+
|
|
20
|
+
// addOnBeforeAll(fieldControlConfigurations, (req) => {
|
|
21
|
+
// this.fetchS4Data
|
|
22
|
+
// })
|
|
23
|
+
|
|
24
|
+
module.exports = fieldControlConfigurations;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { fieldControlDictionary } = require('cap-domain');
|
|
2
|
+
|
|
3
|
+
const fieldControlConfigurations = {
|
|
4
|
+
title: {
|
|
5
|
+
fc: () => fieldControlDictionary.Mandatory,
|
|
6
|
+
validator(value, { i18n }) {
|
|
7
|
+
if (!value || value.length < 10) {
|
|
8
|
+
return i18n.getText(
|
|
9
|
+
'book.validation.title.minLength'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
onBeforeSave(entity, entityChanges) { },
|
|
14
|
+
},
|
|
15
|
+
description: {
|
|
16
|
+
fc: (book) => book.enableDetails ? fieldControlDictionary.Mandatory : fieldControlDictionary.Hidden,
|
|
17
|
+
},
|
|
18
|
+
enableDetails: {
|
|
19
|
+
fc: () => fieldControlDictionary.Optional,
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
module.exports = fieldControlConfigurations;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { fieldControlDictionary } = require('cap-domain');
|
|
2
|
+
|
|
3
|
+
const fieldControlConfigurations = {
|
|
4
|
+
title: {
|
|
5
|
+
fc: () => fieldControlDictionary.Mandatory,
|
|
6
|
+
validator(value, { i18n }) {
|
|
7
|
+
if (!value || value.length < 10) {
|
|
8
|
+
return i18n.getText(
|
|
9
|
+
'book.validation.title.minLength'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
module.exports = fieldControlConfigurations;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const { fieldControlDictionary } = require('cap-domain');
|
|
2
|
+
|
|
3
|
+
const fieldControlConfigurations = {
|
|
4
|
+
title: {
|
|
5
|
+
fc: () => fieldControlDictionary.Mandatory,
|
|
6
|
+
validator(value, { i18n }) {
|
|
7
|
+
if (!value || value.length < 10) {
|
|
8
|
+
return i18n.getText(
|
|
9
|
+
'book.validation.title.minLength'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
archived: {
|
|
15
|
+
fc: () => fieldControlDictionary.Optional,
|
|
16
|
+
},
|
|
17
|
+
internalNotes: {
|
|
18
|
+
fc: (request) => request.archived ? fieldControlDictionary.Hidden : fieldControlDictionary.Optional,
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = fieldControlConfigurations;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
using {CatalogService as srv} from './CatalogService';
|
|
2
|
+
|
|
3
|
+
annotate srv.Books with {
|
|
4
|
+
title @(
|
|
5
|
+
Common.FieldControl: {$value: title_fc},
|
|
6
|
+
Common.Label : '{i18n>title}'
|
|
7
|
+
);
|
|
8
|
+
description @(
|
|
9
|
+
Common.FieldControl: {$value: description_fc},
|
|
10
|
+
Common.Label : '{i18n>description}'
|
|
11
|
+
);
|
|
12
|
+
enableDetails @(
|
|
13
|
+
Common.FieldControl: {$value: enableDetails_fc}
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
annotate srv.Request with {
|
|
18
|
+
title @(
|
|
19
|
+
Common.FieldControl: {$value: title_fc},
|
|
20
|
+
Common.Label : '{i18n>title}'
|
|
21
|
+
);
|
|
22
|
+
archived @(
|
|
23
|
+
Common.FieldControl: {$value: archived_fc}
|
|
24
|
+
);
|
|
25
|
+
internalNotes @(
|
|
26
|
+
Common.FieldControl: {$value: internalNotes_fc}
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
annotate srv.Details with {
|
|
31
|
+
title @(
|
|
32
|
+
Common.FieldControl: {$value: title_fc},
|
|
33
|
+
Common.Label : '{i18n>title}'
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
annotate srv.Authors with {
|
|
39
|
+
name @(
|
|
40
|
+
Common.FieldControl: {$value: name_fc},
|
|
41
|
+
Common.Label : '{i18n>name}'
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
using my.bookshop as my from '../db/data-model';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
service CatalogService {
|
|
5
|
+
@FCSettings: {
|
|
6
|
+
path : 'srv/@FCDefinitions/Authors.js'
|
|
7
|
+
}
|
|
8
|
+
entity Authors as
|
|
9
|
+
projection on my.Authors {
|
|
10
|
+
*,
|
|
11
|
+
virtual null as name_fc : Integer @odata.Type: 'Edm.Byte',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
@FCSettings: {
|
|
15
|
+
path : 'srv/@FCDefinitions/Books.js'
|
|
16
|
+
}
|
|
17
|
+
entity Books as
|
|
18
|
+
projection on my.Books {
|
|
19
|
+
*,
|
|
20
|
+
virtual null as title_fc : Integer @odata.Type: 'Edm.Byte',
|
|
21
|
+
virtual null as description_fc : Integer @odata.Type: 'Edm.Byte',
|
|
22
|
+
virtual null as enableDetails_fc : Integer @odata.Type: 'Edm.Byte'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
@FCSettings: {
|
|
26
|
+
path : 'srv/@FCDefinitions/Request.js',
|
|
27
|
+
autoErase : false,
|
|
28
|
+
blockUnannotatedValueChanges: false
|
|
29
|
+
}
|
|
30
|
+
entity Request as
|
|
31
|
+
projection on my.Request {
|
|
32
|
+
*,
|
|
33
|
+
virtual null as title_fc : Integer @odata.Type: 'Edm.Byte',
|
|
34
|
+
virtual null as archived_fc : Integer @odata.Type: 'Edm.Byte',
|
|
35
|
+
virtual null as internalNotes_fc : Integer @odata.Type: 'Edm.Byte'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
@FCSettings: {
|
|
39
|
+
path : 'srv/@FCDefinitions/Details.js'
|
|
40
|
+
}
|
|
41
|
+
entity Details as
|
|
42
|
+
projection on my.Details {
|
|
43
|
+
*,
|
|
44
|
+
virtual null as title_fc : Integer @odata.Type: 'Edm.Byte',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const { execAfterREADHandler, execUPDATEHandler, validateAndThowErrorsIfExists } = require('cap-domain');
|
|
3
|
+
|
|
4
|
+
class CatalogService extends cds.ApplicationService {
|
|
5
|
+
init() {
|
|
6
|
+
const { Books, Authors, Request, Details } = this.entities;
|
|
7
|
+
|
|
8
|
+
for (const entity of [ Books, Authors, Request, Details ]) {
|
|
9
|
+
this.on('CREATE', entity, async (req, next) => {
|
|
10
|
+
await validateAndThowErrorsIfExists(req, req.data, 'in');
|
|
11
|
+
return await next();
|
|
12
|
+
});
|
|
13
|
+
this.after('READ', entity, execAfterREADHandler);
|
|
14
|
+
this.on('UPDATE', entity, execUPDATEHandler);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return super.init();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = CatalogService;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
title=Titel
|
|
2
|
+
description=Beschreibung
|
|
3
|
+
name=Name X
|
|
4
|
+
book.validation.title.minLength=Der Titel darf nicht kürzer als 10 Zeichen sein
|
|
5
|
+
capdomain.validation.message.readOnly={0} ist ein schreibgeschütztes Feld
|
|
6
|
+
capdomain.validation.message.required={0} ist ein Pflichtfeld
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const { GET, PATCH, POST, DELETE } = cds.test(__dirname + '/..');
|
|
3
|
+
|
|
4
|
+
// should be in sync with mock users in package.json
|
|
5
|
+
const users = {
|
|
6
|
+
admin: {
|
|
7
|
+
username: 'admin',
|
|
8
|
+
password: 'password',
|
|
9
|
+
email: 'admin@example.com',
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const services = {
|
|
14
|
+
catalog: '/odata/v4/catalog'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const STATUS_BY_METHOD = {
|
|
18
|
+
GET: 200,
|
|
19
|
+
POST: 201,
|
|
20
|
+
PATCH: 200,
|
|
21
|
+
PUT: 200,
|
|
22
|
+
DELETE: 204,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class ServiceAPI {
|
|
26
|
+
constructor(servicePath, authConfig) {
|
|
27
|
+
this.servicePath = servicePath;
|
|
28
|
+
this.authConfig = authConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async GET(url, config = {}) {
|
|
32
|
+
return await GET(`${this.servicePath}${url}`, Object.assign(config, this.authConfig));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async PATCH(url, data = {}, config = {}) {
|
|
36
|
+
try {
|
|
37
|
+
return await PATCH(`${this.servicePath}${url}`, data, Object.assign(config, this.authConfig));
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { err };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async POST(url, data = {}, config = {}) {
|
|
44
|
+
try {
|
|
45
|
+
return await POST(`${this.servicePath}${url}`, data, Object.assign(config, this.authConfig));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return { err };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async DELETE(url, config = {}) {
|
|
52
|
+
try {
|
|
53
|
+
return await DELETE(`${this.servicePath}${url}`, Object.assign(config, this.authConfig));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return { err };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async assertRequest(method, url, bodyOrCheck, check) {
|
|
60
|
+
let body, checkFn;
|
|
61
|
+
if (typeof bodyOrCheck === 'function') {
|
|
62
|
+
checkFn = bodyOrCheck;
|
|
63
|
+
} else {
|
|
64
|
+
body = bodyOrCheck;
|
|
65
|
+
checkFn = check;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const res = await this[method](url, ...(body ? [body] : []));
|
|
69
|
+
|
|
70
|
+
// expect(res.status).toEqual(STATUS_BY_METHOD[method] ?? 200);
|
|
71
|
+
checkFn?.(res);
|
|
72
|
+
return res;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
assertGet(url, check) { return this.assertRequest('GET', url, check); }
|
|
76
|
+
assertPost(url, body, check) { return this.assertRequest('POST', url, body, check); }
|
|
77
|
+
assertPatch(url, body, check) { return this.assertRequest('PATCH', url, body, check); }
|
|
78
|
+
assertDelete(url, check) { return this.assertRequest('DELETE', url, check); }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
class UserAPI {
|
|
82
|
+
constructor(config) {
|
|
83
|
+
this.email = config.email;
|
|
84
|
+
this.authConfig = {
|
|
85
|
+
auth: config,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (const serviceName in services) {
|
|
89
|
+
this[serviceName] = new ServiceAPI(services[serviceName], this.authConfig);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {
|
|
95
|
+
users,
|
|
96
|
+
admin: new UserAPI(users.admin)
|
|
97
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { admin } = require("./UsersApi");
|
|
2
|
+
|
|
3
|
+
describe("Authors", () => {
|
|
4
|
+
it("rejects a name shorter than the configured minimum length", async () => {
|
|
5
|
+
await admin.catalog.assertPost("/Authors", { name: "Al" }, (res) => {
|
|
6
|
+
expect(res).toBeFailed();
|
|
7
|
+
expect(res?.err?.response?.data?.error?.message).toBeTruthy();
|
|
8
|
+
});
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("accepts a name that satisfies the validator", async () => {
|
|
12
|
+
const payload = { name: "Alexander Pushkin" };
|
|
13
|
+
|
|
14
|
+
await admin.catalog.assertPost("/Authors", payload, (res) => {
|
|
15
|
+
expect(res.data).toMatchSubset(payload);
|
|
16
|
+
expect(res).toBeSuccessful();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("computes the mandatory field control for name on READ", async () => {
|
|
21
|
+
await admin.catalog.assertGet("/Authors", (res) => {
|
|
22
|
+
expect(res).toBeSuccessful();
|
|
23
|
+
res.data.value.forEach((author) => expect(author.name_fc).toEqual(7));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const { admin } = require("./UsersApi");
|
|
2
|
+
|
|
3
|
+
describe("Books", () => {
|
|
4
|
+
it("success flow", async () => {
|
|
5
|
+
const payload = {
|
|
6
|
+
title: "12345678901",
|
|
7
|
+
enableDetails: true,
|
|
8
|
+
description: "This is a description that should be mandatory when enableDetails is true."
|
|
9
|
+
};
|
|
10
|
+
await admin.catalog.assertPost("/Books", payload, (res) => {
|
|
11
|
+
expect(res.data).toMatchSubset(payload);
|
|
12
|
+
expect(res).toBeSuccessful();
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("failure flow", async () => {
|
|
17
|
+
const payload = {
|
|
18
|
+
title: "1",
|
|
19
|
+
enableDetails: true
|
|
20
|
+
};
|
|
21
|
+
await admin.catalog.assertPost("/Books", payload, (res) => {
|
|
22
|
+
expect(res).toBeFailed();
|
|
23
|
+
expect(res).toHaveErrorMessagesCount(2);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns the localized validation message text", async () => {
|
|
28
|
+
const payload = { title: "1" };
|
|
29
|
+
|
|
30
|
+
await admin.catalog.assertPost("/Books", payload, (res) => {
|
|
31
|
+
expect(res).toBeFailed();
|
|
32
|
+
expect(res?.err?.response?.data?.error?.message).toEqual("Title cannot be less than 10 characters");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("computes field controls on READ based on entity state", async () => {
|
|
37
|
+
await admin.catalog.assertGet("/Books", (res) => {
|
|
38
|
+
expect(res).toBeSuccessful();
|
|
39
|
+
|
|
40
|
+
const withDetails = res.data.value.find((b) => b.title === "Wuthering Heights");
|
|
41
|
+
const withoutDetails = res.data.value.find((b) => b.title === "Jane Eyre");
|
|
42
|
+
|
|
43
|
+
// enableDetails=true -> Mandatory(7), enableDetails=false -> Hidden(0)
|
|
44
|
+
expect(withDetails.description_fc).toEqual(7);
|
|
45
|
+
expect(withoutDetails.description_fc).toEqual(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("blocks updates to fields that became read-only/hidden", async () => {
|
|
50
|
+
const created = await admin.catalog.assertPost("/Books", {
|
|
51
|
+
title: "A Read Only Field Test",
|
|
52
|
+
enableDetails: false
|
|
53
|
+
});
|
|
54
|
+
const { ID } = created.data;
|
|
55
|
+
|
|
56
|
+
await admin.catalog.assertPatch(`/Books(${ID})`, { description: "should not be allowed" }, (res) => {
|
|
57
|
+
expect(res).toBeFailed();
|
|
58
|
+
expect(res?.err?.response?.data?.error?.message).toEqual("Description is Read-only field");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("auto-erases field values once they become hidden", async () => {
|
|
63
|
+
const created = await admin.catalog.assertPost("/Books", {
|
|
64
|
+
title: "Auto Erase Field Test",
|
|
65
|
+
enableDetails: true,
|
|
66
|
+
description: "A description that is long enough to be considered valid."
|
|
67
|
+
});
|
|
68
|
+
const { ID } = created.data;
|
|
69
|
+
|
|
70
|
+
await admin.catalog.assertPatch(`/Books(${ID})`, { enableDetails: false }, (res) => {
|
|
71
|
+
expect(res).toBeSuccessful();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await admin.catalog.assertGet(`/Books(${ID})`, (res) => {
|
|
75
|
+
expect(res).toBeSuccessful();
|
|
76
|
+
expect(res.data.description).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("blocks changes to fields without a field control annotation", async () => {
|
|
81
|
+
const created = await admin.catalog.assertPost("/Books", {
|
|
82
|
+
title: "Unannotated Field Test",
|
|
83
|
+
enableDetails: false
|
|
84
|
+
});
|
|
85
|
+
const { ID } = created.data;
|
|
86
|
+
|
|
87
|
+
await admin.catalog.assertPatch(`/Books(${ID})`, { stock: 999 }, (res) => {
|
|
88
|
+
expect(res).toBeSuccessful();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await admin.catalog.assertGet(`/Books(${ID})`, (res) => {
|
|
92
|
+
expect(res).toBeSuccessful();
|
|
93
|
+
expect(res.data.stock).not.toEqual(999);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const { admin } = require("./UsersApi");
|
|
2
|
+
|
|
3
|
+
// Request entity is configured (via @FCSettings) with autoErase:false and
|
|
4
|
+
// blockUnannotatedValueChanges:false, to demonstrate both flags can be turned off.
|
|
5
|
+
describe("Request (autoErase/blockUnannotatedValueChanges disabled)", () => {
|
|
6
|
+
it("does not erase a field's value once it becomes hidden when autoErase is false", async () => {
|
|
7
|
+
const created = await admin.catalog.assertPost("/Request", {
|
|
8
|
+
title: "Auto Erase Disabled Test",
|
|
9
|
+
archived: false,
|
|
10
|
+
internalNotes: "Keep me even after archiving"
|
|
11
|
+
});
|
|
12
|
+
const { ID } = created.data;
|
|
13
|
+
|
|
14
|
+
await admin.catalog.assertPatch(`/Request(${ID})`, { archived: true }, (res) => {
|
|
15
|
+
expect(res).toBeSuccessful();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
await admin.catalog.assertGet(`/Request(${ID})`, (res) => {
|
|
19
|
+
expect(res).toBeSuccessful();
|
|
20
|
+
expect(res.data.internalNotes).toEqual("Keep me even after archiving");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("allows changes to fields without a field control annotation when blockUnannotatedValueChanges is false", async () => {
|
|
25
|
+
const created = await admin.catalog.assertPost("/Request", {
|
|
26
|
+
title: "Unannotated Field Allowed Test"
|
|
27
|
+
});
|
|
28
|
+
const { ID } = created.data;
|
|
29
|
+
|
|
30
|
+
await admin.catalog.assertPatch(`/Request(${ID})`, { note: "freeform note" }, (res) => {
|
|
31
|
+
expect(res).toBeSuccessful();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await admin.catalog.assertGet(`/Request(${ID})`, (res) => {
|
|
35
|
+
expect(res).toBeSuccessful();
|
|
36
|
+
expect(res.data.note).toEqual("freeform note");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
expect.extend({
|
|
2
|
+
toMatchSubset(received, expected) {
|
|
3
|
+
const isSubset = (A, B) =>
|
|
4
|
+
Object.entries(B).every(([key, val]) =>
|
|
5
|
+
val && typeof val === "object"
|
|
6
|
+
? isSubset(A[key] ?? {}, val)
|
|
7
|
+
: A[key] === val
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const pass = received != null && isSubset(received, expected);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
pass,
|
|
14
|
+
message: () =>
|
|
15
|
+
pass
|
|
16
|
+
? `expected received object NOT to contain all fields of expected, but it did.\nExpected: ${JSON.stringify(expected, null, 2)}\nReceived: ${JSON.stringify(received, null, 2)}`
|
|
17
|
+
: `expected received object to contain all fields of expected.\nExpected: ${JSON.stringify(expected, null, 2)}\nReceived: ${JSON.stringify(received, null, 2)}\nMissing or mismatched keys: ${Object.entries(expected)
|
|
18
|
+
.filter(([key, val]) => received[key] !== val)
|
|
19
|
+
.map(([key]) => key)
|
|
20
|
+
.join(", ")
|
|
21
|
+
}`,
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
toBeSuccessful(res) {
|
|
26
|
+
const status = res?.status;
|
|
27
|
+
const pass = status >= 200 && status <= 299;
|
|
28
|
+
return {
|
|
29
|
+
pass,
|
|
30
|
+
message: () =>
|
|
31
|
+
`expected status ${status} to be in 2xx range, but received ${JSON.stringify(res?.err?.response?.data ?? res?.data)}`,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
toBeFailed(res) {
|
|
36
|
+
const status = res?.err?.status;
|
|
37
|
+
const pass = status >= 400 && status <= 499;
|
|
38
|
+
return {
|
|
39
|
+
pass,
|
|
40
|
+
message: () =>
|
|
41
|
+
`expected status ${status} to be in 4xx range, but received ${JSON.stringify(res?.err?.response?.data ?? res?.data)}`,
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
toHaveErrorMessagesCount(res, expectedCount) {
|
|
46
|
+
const errors = res?.err?.response?.data?.error?.details || [];
|
|
47
|
+
const pass = errors.length === expectedCount;
|
|
48
|
+
return {
|
|
49
|
+
pass,
|
|
50
|
+
message: () =>
|
|
51
|
+
`expected error messages count to be ${expectedCount}, but received ${errors.length}. Errors: ${JSON.stringify(errors)}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
});
|