@aerokit/sdk 14.62.0 → 14.64.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/package.json +1 -1
- package/test/api.js +8 -0
- package/test/flows/crud.js +4 -2
- package/test/flows/my.js +7 -3
- package/test/flows/rest.js +10 -1
- package/test/sample-values.js +33 -4
package/package.json
CHANGED
package/test/api.js
CHANGED
|
@@ -21,11 +21,19 @@ export function makeApi(request, manifest) {
|
|
|
21
21
|
getResponse: (entity, id) => request.get(url(entity, '/' + id)),
|
|
22
22
|
create: (entity, data) => request.post(url(entity), { data }).then(asJson),
|
|
23
23
|
update: (entity, id, data) => request.put(url(entity, '/' + id), { data }).then(asJson),
|
|
24
|
+
// Answers whether the row is gone. A record whose process declares `whenDeleted: refuse` is
|
|
25
|
+
// REFUSED with 409 while that instance runs (dirigible #7074) - the guard doing its job, not a
|
|
26
|
+
// failure, so the caller is told rather than thrown at and its cleanup completes either way.
|
|
27
|
+
// Every other failing status still throws.
|
|
24
28
|
remove: async (entity, id) => {
|
|
25
29
|
const response = await request.delete(url(entity, '/' + id));
|
|
30
|
+
if (response.status() === 409 && entity.deleteGuardedByProcess?.length) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
26
33
|
if (!response.ok()) {
|
|
27
34
|
throw new Error(`DELETE ${response.url()} -> ${response.status()} ${await response.text()}`);
|
|
28
35
|
}
|
|
36
|
+
return true;
|
|
29
37
|
},
|
|
30
38
|
};
|
|
31
39
|
}
|
package/test/flows/crud.js
CHANGED
|
@@ -68,8 +68,10 @@ export function crudFlow(manifest, entity, opts = {}) {
|
|
|
68
68
|
record[handle.name] = updated;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// delete (detail pane trash button, then the confirm dialog)
|
|
72
|
-
|
|
71
|
+
// delete (detail pane trash button, then the confirm dialog). An entity whose process declares
|
|
72
|
+
// `whenDeleted: refuse` answers the delete with an error toast while its instance runs - the
|
|
73
|
+
// create above started it - so the walk stops here and the REST flow asserts that refusal.
|
|
74
|
+
if (!skip.has('delete') && !entity.deleteGuardedByProcess?.length) {
|
|
73
75
|
await dataRow(page, record[handle.name]).click();
|
|
74
76
|
await page.getByRole('button', { name: 'Delete', exact: true }).first().click();
|
|
75
77
|
const dialog = page.locator('[x-h-dialog-overlay][data-open]');
|
package/test/flows/my.js
CHANGED
|
@@ -108,6 +108,7 @@ export function myFlow(manifest, entity, opts = {}) {
|
|
|
108
108
|
const id = created?.[idProperty];
|
|
109
109
|
expect(id, 'personal create carries the generated id').toBeTruthy();
|
|
110
110
|
let foreignId;
|
|
111
|
+
let deleted;
|
|
111
112
|
try {
|
|
112
113
|
const own = await client.get(mine, id);
|
|
113
114
|
expect(own[entity.personal.owner], 'the owner FK is forced server-side on a personal create').toBeTruthy();
|
|
@@ -133,11 +134,14 @@ export function myFlow(manifest, entity, opts = {}) {
|
|
|
133
134
|
expect(rowsAfter.some((row) => row[idProperty] === foreignId), 'a foreign row must not appear in the personal list').toBe(false);
|
|
134
135
|
}
|
|
135
136
|
} finally {
|
|
136
|
-
await client.remove(mine, id); // deleting the OWN row through the personal controller works
|
|
137
|
+
deleted = await client.remove(mine, id); // deleting the OWN row through the personal controller works
|
|
137
138
|
if (foreignId) await client.remove(entity, foreignId);
|
|
138
139
|
}
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
// ... unless a `whenDeleted: refuse` process guards the record while its instance runs
|
|
141
|
+
if (deleted) {
|
|
142
|
+
const gone = await api.get(manifest.restBase + mine.api + '/' + id);
|
|
143
|
+
expect(gone.status()).toBe(404);
|
|
144
|
+
}
|
|
141
145
|
});
|
|
142
146
|
|
|
143
147
|
// wave 2: the UI-parity walk over the same personal surface (older manifests without the
|
package/test/flows/rest.js
CHANGED
|
@@ -23,6 +23,7 @@ export function restFlow(manifest, entity, opts = {}) {
|
|
|
23
23
|
const created = await client.create(entity, payload);
|
|
24
24
|
const id = created?.[idProperty];
|
|
25
25
|
expect(id, 'create response carries the generated id').toBeTruthy();
|
|
26
|
+
let deleted;
|
|
26
27
|
try {
|
|
27
28
|
const read = await client.get(entity, id);
|
|
28
29
|
expect(read[idProperty]).toBe(id);
|
|
@@ -36,7 +37,15 @@ export function restFlow(manifest, entity, opts = {}) {
|
|
|
36
37
|
expect(reread[handle.name]).toBe(updatedValue);
|
|
37
38
|
}
|
|
38
39
|
} finally {
|
|
39
|
-
await client.remove(entity, id);
|
|
40
|
+
deleted = await client.remove(entity, id);
|
|
41
|
+
}
|
|
42
|
+
if (!deleted) {
|
|
43
|
+
// the record's process guard refused the delete (409) - assert the other half of that promise:
|
|
44
|
+
// the row is still served. A guarded entity can also delete cleanly, when its instance had
|
|
45
|
+
// already finished, so which outcome is expected is only knowable after the attempt.
|
|
46
|
+
const kept = await client.get(entity, id);
|
|
47
|
+
expect(kept[idProperty], `delete refused by ${entity.deleteGuardedByProcess.join(', ')} - the record must survive`).toBe(id);
|
|
48
|
+
return;
|
|
40
49
|
}
|
|
41
50
|
const gone = await client.getResponse(entity, id);
|
|
42
51
|
expect(gone.status()).toBe(404);
|
package/test/sample-values.js
CHANGED
|
@@ -14,8 +14,8 @@ export function sampleValue(field) {
|
|
|
14
14
|
switch (field.type) {
|
|
15
15
|
case 'string': {
|
|
16
16
|
const len = field.length ?? 64;
|
|
17
|
-
|
|
18
|
-
return
|
|
17
|
+
const value = len >= 16 ? 'APPTEST-' + rand(ALPHA + DIGITS, 6) : rand(ALPHA, Math.max(1, len - 1)) + rand(DIGITS, 1);
|
|
18
|
+
return field.pattern ? shaped(field, value) : value;
|
|
19
19
|
}
|
|
20
20
|
case 'integer':
|
|
21
21
|
case 'bigint':
|
|
@@ -37,6 +37,31 @@ export function sampleValue(field) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// A `pattern:` field (an authored regex, or the address regex behind `format: email`) is rejected
|
|
41
|
+
// by the generated controller with 400 unless the value has the declared shape, so the marker value
|
|
42
|
+
// above has to be traded for one that matches: the candidates are tried in order and the first that
|
|
43
|
+
// both matches and fits the declared length wins. A pattern nothing here matches keeps the plain
|
|
44
|
+
// marker - the controller's own 400 then names the field and the pattern, which is the message the
|
|
45
|
+
// module author needs, and a silent near-miss would not.
|
|
46
|
+
function shaped(field, value) {
|
|
47
|
+
let regex;
|
|
48
|
+
try {
|
|
49
|
+
// anchored like the generated controller's own String.matches, so an unanchored pattern cannot
|
|
50
|
+
// pass here on a substring and then be refused by the server
|
|
51
|
+
regex = new RegExp('^(?:' + field.pattern + ')$');
|
|
52
|
+
} catch {
|
|
53
|
+
return value; // not a JavaScript-parsable regex - nothing to shape the value to
|
|
54
|
+
}
|
|
55
|
+
const token = value.replace(/[^A-Za-z0-9]/g, '');
|
|
56
|
+
const candidates = [
|
|
57
|
+
value,
|
|
58
|
+
`apptest.${token}@apptest.example.com`, // format: email and other address-shaped patterns
|
|
59
|
+
token, // letters and digits, no separator
|
|
60
|
+
rand(DIGITS, 10), // a numeric code
|
|
61
|
+
];
|
|
62
|
+
return candidates.find((candidate) => candidate.length <= (field.length ?? 255) && regex.test(candidate)) ?? value;
|
|
63
|
+
}
|
|
64
|
+
|
|
40
65
|
export function editableFields(entity) {
|
|
41
66
|
return (entity.fields ?? []).filter((f) => !f.readOnly && !f.primaryKey && !f.generated);
|
|
42
67
|
}
|
|
@@ -108,11 +133,15 @@ function shifted(value, type, step) {
|
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
// The searchable "handle" field: the first long string field shown in the list. Its
|
|
111
|
-
// value identifies the record in the table across the create/edit/delete flow
|
|
136
|
+
// value identifies the record in the table across the create/edit/delete flow, and the flows
|
|
137
|
+
// flip it by appending a suffix - so a field carrying a `pattern:` cannot be it, the suffix
|
|
138
|
+
// breaking the very shape the controller enforces. Read-only fields are out through
|
|
139
|
+
// editableFields: a `number:` field is the platform's to stamp and preserve, so a write to it
|
|
140
|
+
// reads back unchanged (dirigible #7411).
|
|
112
141
|
// Null when the entity has no such field (all-numeric/date entities) - flows degrade:
|
|
113
142
|
// the UI walk is skipped and the REST flow drops its update-value assertion.
|
|
114
143
|
export function handleField(entity) {
|
|
115
|
-
return editableFields(entity).find((f) => f.type === 'string' && (f.length ?? 64) >= 16 && f.major !== false) ?? null;
|
|
144
|
+
return editableFields(entity).find((f) => f.type === 'string' && (f.length ?? 64) >= 16 && f.major !== false && !f.pattern) ?? null;
|
|
116
145
|
}
|
|
117
146
|
|
|
118
147
|
export function labelOf(field) {
|