@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerokit/sdk",
3
- "version": "14.62.0",
3
+ "version": "14.64.0",
4
4
  "description": "Unified TypeScript SDK for modern cloud platforms.",
5
5
  "scripts": {
6
6
  "clean": "rm -rf dist",
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
  }
@@ -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
- if (!skip.has('delete')) {
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
- const gone = await api.get(manifest.restBase + mine.api + '/' + id);
140
- expect(gone.status()).toBe(404);
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
@@ -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);
@@ -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
- if (len >= 16) return 'APPTEST-' + rand(ALPHA + DIGITS, 6);
18
- return rand(ALPHA, Math.max(1, len - 1)) + rand(DIGITS, 1);
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) {