@owlmeans/resource 0.1.18-rc.35 → 0.1.18-rc.37
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/README.md +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/resource/SKILL.md +15 -2
- package/build/errors.d.ts +13 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +15 -1
- package/build/errors.js.map +1 -1
- package/package.json +5 -3
- package/src/errors.ts +16 -1
- package/tests/errors.spec.d.ts +2 -0
- package/tests/errors.spec.d.ts.map +1 -0
- package/tests/errors.spec.js +51 -0
- package/tests/errors.spec.js.map +1 -0
- package/tests/errors.spec.ts +60 -0
- package/tests/tsconfig.json +17 -0
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ wants is the `resource-choice` decision.
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/resource@^0.1.18-rc.
|
|
15
|
+
bun add @owlmeans/resource@^0.1.18-rc.37
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Concepts
|
|
@@ -403,7 +403,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
403
403
|
your project's skill store (`.agents/skills/`):
|
|
404
404
|
|
|
405
405
|
```sh
|
|
406
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
406
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.42
|
|
407
407
|
```
|
|
408
408
|
|
|
409
409
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/resource",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.37",
|
|
5
|
+
"generatedAt": "2026-09-29T08:01:58.119Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/resource
|
|
9
9
|
|
|
10
10
|
**Layer:** Core
|
|
11
|
-
**Install:** `"@owlmeans/resource": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/resource": "^0.1.18-rc.37"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -30,7 +30,7 @@ user-invocable: false
|
|
|
30
30
|
| `ResourceDbService<Db, Client>`, `DbLocker<T>` | What a connection service implements: `db`/`client`/`clients`/`config`/`name`/`ensureConfigAlias`/`initialize`, plus `lock`/`unlock` when the backend encrypts fields. |
|
|
31
31
|
| `ResourceMaker<R, T>` | `(dbAlias?, serviceAlias?) => T` — the signature every resource maker is typed with, so an app registers `makeXResource()` without repeating the argument list. |
|
|
32
32
|
| `filterObject(obj, keep?)`, `createListSchema` | Drop null and undefined properties, keeping the names listed in `keep`; the AJV schema for a `ListResult<T>` envelope. |
|
|
33
|
-
| Errors | `ResourceError`, `UnknownRecordError
|
|
33
|
+
| Errors | `ResourceError`, `UnknownRecordError` (404), `MisshapedRecord`, `RecordExists` (409), `RecordUpdateFailed`, `UnsupportedArgumentError`, `UnsupportedMethodError`, `MigrationError`, `MigrationConflict` — the unmarked ones are faults (500). `tests/errors.spec.ts` refuses an exported class whose status was not decided. `UnknownRecordError.id` is everything after the first `/`, so an id or criteria text holding one survives. |
|
|
34
34
|
| `DbConfig`, `Config`, `Context` | Database config (`cfg.dbs`) types. |
|
|
35
35
|
|
|
36
36
|
## Usage
|
|
@@ -85,6 +85,19 @@ else; the criteria overload takes `{ sort }` to say which match is "the" one:
|
|
|
85
85
|
Every method returns the record(s) themselves, never a driver result object — there is no
|
|
86
86
|
`rowsAffected`/`rowCount` anywhere on this contract.
|
|
87
87
|
|
|
88
|
+
### What an escaped storage error answers
|
|
89
|
+
|
|
90
|
+
The two refusals declare their HTTP status (`static httpStatus`, the `error` skill's rule), so an
|
|
91
|
+
`@owlmeans/server-api` handler that lets one escape answers it: `UnknownRecordError` **404**,
|
|
92
|
+
`RecordExists` **409** (a unique-key violation, a taken id, and a `create` handed an id —
|
|
93
|
+
`id-present` — alike). Every other class of the family is a fault and answers 500. A development
|
|
94
|
+
body rebuilds into the same class on the client; a production body is the incident id alone and
|
|
95
|
+
arrives as `ApiStatusError` with that status (`@owlmeans/api`).
|
|
96
|
+
|
|
97
|
+
So `get` only what the REQUEST addressed. A missing row it did not name — a configuration record,
|
|
98
|
+
a session, an internal lookup — is not the caller's 404: `load` it and throw the fault or the
|
|
99
|
+
refusal that fits (an auth path throws `AuthorizationError`).
|
|
100
|
+
|
|
88
101
|
### The criteria language
|
|
89
102
|
|
|
90
103
|
`Criteria<T>` is keyed by the record's own fields, so a typo is a compile error. A dotted key
|
package/build/errors.d.ts
CHANGED
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
/**
|
|
3
|
+
* The storage refusal family. A class that answers the caller's request declares the HTTP status
|
|
4
|
+
* an `@owlmeans/server-api` boundary answers it with (`static httpStatus`, inherited through the
|
|
5
|
+
* constructor chain): an absent record 404, a taken id or unique key 409. A fault — a misshaped
|
|
6
|
+
* record, an unsupported argument, a failed migration — declares nothing and answers 500.
|
|
7
|
+
*/
|
|
2
8
|
export declare class ResourceError extends ResilientError {
|
|
3
9
|
static typeName: string;
|
|
4
10
|
constructor(message?: string);
|
|
5
11
|
}
|
|
12
|
+
/** The addressed record does not exist (`get`, `update`, `take` of an absent id or criteria). */
|
|
6
13
|
export declare class UnknownRecordError extends ResourceError {
|
|
7
14
|
static typeName: string;
|
|
15
|
+
/** The addressed record is absent: answered 404. */
|
|
16
|
+
static httpStatus: number;
|
|
8
17
|
static readonly idSeparator: string;
|
|
9
18
|
constructor(id: string);
|
|
19
|
+
/** Everything after the first separator — an id or a criteria description may contain one. */
|
|
10
20
|
get id(): string;
|
|
11
21
|
}
|
|
12
22
|
export declare class MisshapedRecord extends ResourceError {
|
|
13
23
|
static typeName: string;
|
|
14
24
|
constructor(msg: string);
|
|
15
25
|
}
|
|
26
|
+
/** A record under that id or unique key is already stored (`create`, a unique-index violation). */
|
|
16
27
|
export declare class RecordExists extends ResourceError {
|
|
17
28
|
static typeName: string;
|
|
29
|
+
/** The id or unique key is taken: answered 409. */
|
|
30
|
+
static httpStatus: number;
|
|
18
31
|
constructor(msg: string);
|
|
19
32
|
}
|
|
20
33
|
export declare class RecordUpdateFailed extends ResourceError {
|
package/build/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAG,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEjD,qBAAa,aAAc,SAAQ,cAAc;IAC/C,OAAuB,QAAQ,SAAkB;IAEjD,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF;AAED,qBAAa,kBAAmB,SAAQ,aAAa;IACnD,OAAuB,QAAQ,SAAgD;
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAG,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEjD;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,cAAc;IAC/C,OAAuB,QAAQ,SAAkB;IAEjD,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF;AAED,iGAAiG;AACjG,qBAAa,kBAAmB,SAAQ,aAAa;IACnD,OAAuB,QAAQ,SAAgD;IAC/E,oDAAoD;IACpD,OAAc,UAAU,SAAM;IAE9B,gBAAuB,WAAW,EAAE,MAAM,CAAM;IAEhD,YAAY,EAAE,EAAE,MAAM,EAGrB;IAED,8FAA8F;IAC9F,IAAI,EAAE,IAAI,MAAM,CAIf;CACF;AAED,qBAAa,eAAgB,SAAQ,aAAa;IAChD,OAAuB,QAAQ,SAA6C;IAE5E,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF;AAED,mGAAmG;AACnG,qBAAa,YAAa,SAAQ,aAAa;IAC7C,OAAuB,QAAQ,SAA0C;IACzE,mDAAmD;IACnD,OAAc,UAAU,SAAM;IAE9B,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF;AAED,qBAAa,kBAAmB,SAAQ,aAAa;IACnD,OAAuB,QAAQ,SAAgD;IAE/E,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF;AAED,qBAAa,wBAAyB,SAAQ,aAAa;IACzD,OAAuB,QAAQ,SAAsD;IAErF,YAAY,QAAQ,EAAE,MAAM,EAG3B;CACF;AAED,qBAAa,sBAAuB,SAAQ,aAAa;IACvD,OAAuB,QAAQ,SAAoD;IAEnF,YAAY,MAAM,EAAE,MAAM,EAGzB;CACF;AAED;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,aAAa;IAC/C,OAAuB,QAAQ,SAA4C;IAE3E,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,aAAa;IAClD,OAAuB,QAAQ,SAA+C;IAE9E,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF"}
|
package/build/errors.js
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
/**
|
|
3
|
+
* The storage refusal family. A class that answers the caller's request declares the HTTP status
|
|
4
|
+
* an `@owlmeans/server-api` boundary answers it with (`static httpStatus`, inherited through the
|
|
5
|
+
* constructor chain): an absent record 404, a taken id or unique key 409. A fault — a misshaped
|
|
6
|
+
* record, an unsupported argument, a failed migration — declares nothing and answers 500.
|
|
7
|
+
*/
|
|
2
8
|
export class ResourceError extends ResilientError {
|
|
3
9
|
static typeName = 'ResourceError';
|
|
4
10
|
constructor(message = 'error') {
|
|
5
11
|
super(ResourceError.typeName, `resource:${message}`);
|
|
6
12
|
}
|
|
7
13
|
}
|
|
14
|
+
/** The addressed record does not exist (`get`, `update`, `take` of an absent id or criteria). */
|
|
8
15
|
export class UnknownRecordError extends ResourceError {
|
|
9
16
|
static typeName = `${ResourceError.typeName}UnknownRecordError`;
|
|
17
|
+
/** The addressed record is absent: answered 404. */
|
|
18
|
+
static httpStatus = 404;
|
|
10
19
|
static idSeparator = '/';
|
|
11
20
|
constructor(id) {
|
|
12
21
|
super(`unknown-record${UnknownRecordError.idSeparator}${id}`);
|
|
13
22
|
this.type = UnknownRecordError.typeName;
|
|
14
23
|
}
|
|
24
|
+
/** Everything after the first separator — an id or a criteria description may contain one. */
|
|
15
25
|
get id() {
|
|
16
|
-
|
|
26
|
+
const at = this.message.indexOf(UnknownRecordError.idSeparator);
|
|
27
|
+
return at < 0 ? '' : this.message.slice(at + UnknownRecordError.idSeparator.length);
|
|
17
28
|
}
|
|
18
29
|
}
|
|
19
30
|
export class MisshapedRecord extends ResourceError {
|
|
@@ -23,8 +34,11 @@ export class MisshapedRecord extends ResourceError {
|
|
|
23
34
|
this.type = MisshapedRecord.typeName;
|
|
24
35
|
}
|
|
25
36
|
}
|
|
37
|
+
/** A record under that id or unique key is already stored (`create`, a unique-index violation). */
|
|
26
38
|
export class RecordExists extends ResourceError {
|
|
27
39
|
static typeName = `${ResourceError.typeName}RecordExists`;
|
|
40
|
+
/** The id or unique key is taken: answered 409. */
|
|
41
|
+
static httpStatus = 409;
|
|
28
42
|
constructor(msg) {
|
|
29
43
|
super(`record-exists:${msg}`);
|
|
30
44
|
this.type = RecordExists.typeName;
|
package/build/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAG,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEjD,MAAM,OAAO,aAAc,SAAQ,cAAc;IACxC,MAAM,CAAU,QAAQ,GAAG,eAAe,CAAA;IAEjD,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,aAAa,CAAC,QAAQ,EAAE,YAAY,OAAO,EAAE,CAAC,CAAA;IACtD,CAAC;CACF;AAED,MAAM,OAAO,kBAAmB,SAAQ,aAAa;IAC5C,MAAM,CAAU,QAAQ,GAAG,GAAG,aAAa,CAAC,QAAQ,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/resource",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.37",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc -b",
|
|
8
8
|
"dev": "sleep 252 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
9
|
-
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty",
|
|
10
|
+
"test": "bun test ./tests"
|
|
10
11
|
},
|
|
11
12
|
"main": "build/index.js",
|
|
12
13
|
"module": "build/index.js",
|
|
@@ -22,13 +23,14 @@
|
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
25
|
"@owlmeans/context": "^0.1.18-rc.32",
|
|
25
|
-
"@owlmeans/error": "^0.1.18-rc.
|
|
26
|
+
"@owlmeans/error": "^0.1.18-rc.36"
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
28
29
|
"ajv": "*"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@owlmeans/dep-config": "workspace:*",
|
|
33
|
+
"@types/bun": "^1.4.0",
|
|
32
34
|
"nodemon": "^3.1.14",
|
|
33
35
|
"typescript": "^7.0.2"
|
|
34
36
|
},
|
package/src/errors.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
import { ResilientError } from '@owlmeans/error'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The storage refusal family. A class that answers the caller's request declares the HTTP status
|
|
6
|
+
* an `@owlmeans/server-api` boundary answers it with (`static httpStatus`, inherited through the
|
|
7
|
+
* constructor chain): an absent record 404, a taken id or unique key 409. A fault — a misshaped
|
|
8
|
+
* record, an unsupported argument, a failed migration — declares nothing and answers 500.
|
|
9
|
+
*/
|
|
4
10
|
export class ResourceError extends ResilientError {
|
|
5
11
|
public static override typeName = 'ResourceError'
|
|
6
12
|
|
|
@@ -9,8 +15,11 @@ export class ResourceError extends ResilientError {
|
|
|
9
15
|
}
|
|
10
16
|
}
|
|
11
17
|
|
|
18
|
+
/** The addressed record does not exist (`get`, `update`, `take` of an absent id or criteria). */
|
|
12
19
|
export class UnknownRecordError extends ResourceError {
|
|
13
20
|
public static override typeName = `${ResourceError.typeName}UnknownRecordError`
|
|
21
|
+
/** The addressed record is absent: answered 404. */
|
|
22
|
+
public static httpStatus = 404
|
|
14
23
|
|
|
15
24
|
public static readonly idSeparator: string = '/'
|
|
16
25
|
|
|
@@ -19,8 +28,11 @@ export class UnknownRecordError extends ResourceError {
|
|
|
19
28
|
this.type = UnknownRecordError.typeName
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
/** Everything after the first separator — an id or a criteria description may contain one. */
|
|
22
32
|
get id(): string {
|
|
23
|
-
|
|
33
|
+
const at = this.message.indexOf(UnknownRecordError.idSeparator)
|
|
34
|
+
|
|
35
|
+
return at < 0 ? '' : this.message.slice(at + UnknownRecordError.idSeparator.length)
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
38
|
|
|
@@ -33,8 +45,11 @@ export class MisshapedRecord extends ResourceError {
|
|
|
33
45
|
}
|
|
34
46
|
}
|
|
35
47
|
|
|
48
|
+
/** A record under that id or unique key is already stored (`create`, a unique-index violation). */
|
|
36
49
|
export class RecordExists extends ResourceError {
|
|
37
50
|
public static override typeName = `${ResourceError.typeName}RecordExists`
|
|
51
|
+
/** The id or unique key is taken: answered 409. */
|
|
52
|
+
public static httpStatus = 409
|
|
38
53
|
|
|
39
54
|
constructor(msg: string) {
|
|
40
55
|
super(`record-exists:${msg}`)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.spec.d.ts","sourceRoot":"","sources":["errors.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { ResilientError } from '@owlmeans/error';
|
|
3
|
+
import * as errors from '../src/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* The status an HTTP boundary (`@owlmeans/server-api` `errorStatus`) reads off a class: a static
|
|
6
|
+
* `httpStatus` inherited through the constructor chain. A refusal of the caller's request declares
|
|
7
|
+
* a 4xx; a fault declares nothing and answers 500.
|
|
8
|
+
*/
|
|
9
|
+
const statusOf = (error) => error.constructor.httpStatus;
|
|
10
|
+
const DECLARED = {
|
|
11
|
+
ResourceError: undefined,
|
|
12
|
+
UnknownRecordError: 404,
|
|
13
|
+
RecordExists: 409,
|
|
14
|
+
MisshapedRecord: undefined,
|
|
15
|
+
RecordUpdateFailed: undefined,
|
|
16
|
+
UnsupportedArgumentError: undefined,
|
|
17
|
+
UnsupportedMethodError: undefined,
|
|
18
|
+
MigrationError: undefined,
|
|
19
|
+
MigrationConflict: undefined,
|
|
20
|
+
};
|
|
21
|
+
const classes = Object.entries(errors).filter(([, value]) => typeof value === 'function' && value.prototype instanceof ResilientError);
|
|
22
|
+
describe('resource errors — declared HTTP statuses', () => {
|
|
23
|
+
test('every class of the family has decided its status', () => {
|
|
24
|
+
expect(classes.map(([name]) => name).sort()).toEqual(Object.keys(DECLARED).sort());
|
|
25
|
+
});
|
|
26
|
+
test('an absent record is 404, a taken id or key 409; faults declare nothing', () => {
|
|
27
|
+
for (const [name, Class] of classes) {
|
|
28
|
+
expect([name, statusOf(new Class('x'))]).toEqual([name, DECLARED[name]]);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
test('an error rebuilt from its marshalled form keeps its class and its status', () => {
|
|
32
|
+
for (const [name, Class] of classes) {
|
|
33
|
+
const back = ResilientError.ensure(new Class('x').marshal());
|
|
34
|
+
expect([name, back.constructor === Class, statusOf(back)]).toEqual([name, true, DECLARED[name]]);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('UnknownRecordError.id', () => {
|
|
39
|
+
test('is everything after the first separator, however many the id carries', () => {
|
|
40
|
+
expect(new errors.UnknownRecordError('abc').id).toBe('abc');
|
|
41
|
+
expect(new errors.UnknownRecordError('shelf/3/hammer').id).toBe('shelf/3/hammer');
|
|
42
|
+
expect(new errors.UnknownRecordError('{"url":"https://example.org/a"}').id)
|
|
43
|
+
.toBe('{"url":"https://example.org/a"}');
|
|
44
|
+
});
|
|
45
|
+
test('survives a marshal round trip', () => {
|
|
46
|
+
const back = ResilientError.ensure(new errors.UnknownRecordError('shelf/3').marshal());
|
|
47
|
+
expect(back).toBeInstanceOf(errors.UnknownRecordError);
|
|
48
|
+
expect(back.id).toBe('shelf/3');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
//# sourceMappingURL=errors.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.spec.js","sourceRoot":"","sources":["errors.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { ResilientError } from '@owlmeans/error'
|
|
3
|
+
import * as errors from '../src/errors.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The status an HTTP boundary (`@owlmeans/server-api` `errorStatus`) reads off a class: a static
|
|
7
|
+
* `httpStatus` inherited through the constructor chain. A refusal of the caller's request declares
|
|
8
|
+
* a 4xx; a fault declares nothing and answers 500.
|
|
9
|
+
*/
|
|
10
|
+
const statusOf = (error: Error): unknown => (error.constructor as { httpStatus?: unknown }).httpStatus
|
|
11
|
+
|
|
12
|
+
const DECLARED: Record<string, number | undefined> = {
|
|
13
|
+
ResourceError: undefined,
|
|
14
|
+
UnknownRecordError: 404,
|
|
15
|
+
RecordExists: 409,
|
|
16
|
+
MisshapedRecord: undefined,
|
|
17
|
+
RecordUpdateFailed: undefined,
|
|
18
|
+
UnsupportedArgumentError: undefined,
|
|
19
|
+
UnsupportedMethodError: undefined,
|
|
20
|
+
MigrationError: undefined,
|
|
21
|
+
MigrationConflict: undefined,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const classes = Object.entries(errors).filter(([, value]) =>
|
|
25
|
+
typeof value === 'function' && value.prototype instanceof ResilientError
|
|
26
|
+
) as [string, new (message: string) => ResilientError][]
|
|
27
|
+
|
|
28
|
+
describe('resource errors — declared HTTP statuses', () => {
|
|
29
|
+
test('every class of the family has decided its status', () => {
|
|
30
|
+
expect(classes.map(([name]) => name).sort()).toEqual(Object.keys(DECLARED).sort())
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('an absent record is 404, a taken id or key 409; faults declare nothing', () => {
|
|
34
|
+
for (const [name, Class] of classes) {
|
|
35
|
+
expect([name, statusOf(new Class('x'))]).toEqual([name, DECLARED[name]])
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('an error rebuilt from its marshalled form keeps its class and its status', () => {
|
|
40
|
+
for (const [name, Class] of classes) {
|
|
41
|
+
const back = ResilientError.ensure(new Class('x').marshal())
|
|
42
|
+
expect([name, back.constructor === Class, statusOf(back)]).toEqual([name, true, DECLARED[name]])
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('UnknownRecordError.id', () => {
|
|
48
|
+
test('is everything after the first separator, however many the id carries', () => {
|
|
49
|
+
expect(new errors.UnknownRecordError('abc').id).toBe('abc')
|
|
50
|
+
expect(new errors.UnknownRecordError('shelf/3/hammer').id).toBe('shelf/3/hammer')
|
|
51
|
+
expect(new errors.UnknownRecordError('{"url":"https://example.org/a"}').id)
|
|
52
|
+
.toBe('{"url":"https://example.org/a"}')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('survives a marshal round trip', () => {
|
|
56
|
+
const back = ResilientError.ensure(new errors.UnknownRecordError('shelf/3').marshal())
|
|
57
|
+
expect(back).toBeInstanceOf(errors.UnknownRecordError)
|
|
58
|
+
expect((back as errors.UnknownRecordError).id).toBe('shelf/3')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json",
|
|
4
|
+
"@owlmeans/dep-config/tsconfig.node.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"types": [
|
|
8
|
+
"bun"
|
|
9
|
+
],
|
|
10
|
+
"rootDir": "../",
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"./**/*",
|
|
15
|
+
"../src/**/*"
|
|
16
|
+
]
|
|
17
|
+
}
|
package/tsconfig.json
CHANGED