@jskit-ai/resource-crud-core 0.1.101 → 0.1.103

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": "@jskit-ai/resource-crud-core",
3
- "version": "0.1.101",
3
+ "version": "0.1.103",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -13,8 +13,8 @@
13
13
  "./shared/crudResource": "./src/shared/crudResource.js"
14
14
  },
15
15
  "dependencies": {
16
- "@jskit-ai/kernel": "0.1.159",
17
- "@jskit-ai/resource-core": "0.1.101"
16
+ "@jskit-ai/kernel": "0.1.161",
17
+ "@jskit-ai/resource-core": "0.1.103"
18
18
  },
19
19
  "description": "Shared CRUD resource, field, lookup, filter, and namespace contracts.",
20
20
  "jskit": {
@@ -32,20 +32,6 @@
32
32
  "client": {
33
33
  "providers": []
34
34
  }
35
- },
36
- "mutations": {
37
- "dependencies": {
38
- "runtime": {
39
- "@jskit-ai/resource-crud-core": "0.1.101"
40
- },
41
- "dev": {}
42
- },
43
- "packageJson": {
44
- "scripts": {}
45
- },
46
- "procfile": {},
47
- "files": [],
48
- "text": []
49
35
  }
50
36
  }
51
37
  }
@@ -0,0 +1,103 @@
1
+ ---
2
+ id: crud/resource-contract
3
+ title: Owner-scoped CRUD resource contract
4
+ summary: Define an authenticated application resource whose records belong to the current user.
5
+ keywords: authenticated, crud, database, owner-scoped, resource, user
6
+ requires: @jskit-ai/resource-crud-core
7
+ ---
8
+
9
+ # Owner-scoped CRUD resource contract
10
+
11
+ ## Use when
12
+
13
+ Use this pattern when an authenticated application stores records that belong
14
+ to one user and standard list, view, create, patch, and delete behaviour fits
15
+ the product.
16
+
17
+ The example is intentionally a normal source file. Copy it directly when the
18
+ shape fits, adapt its fields and language for another entity, or read it as a
19
+ reference for `defineCrudResource()`.
20
+
21
+ ## Do not use when
22
+
23
+ Do not use this pattern for public records, workspace-owned records, inherited
24
+ ownership, composite identities, or a domain whose operations do not match
25
+ normal CRUD behaviour. Select a pattern matching the real ownership and
26
+ operation model instead of changing `autofilter` merely to make a test pass.
27
+
28
+ ## Product decisions
29
+
30
+ Know these decisions before adapting the example:
31
+
32
+ - whether records truly belong to the current user
33
+ - which fields are required, optional, nullable, searchable, or hidden
34
+ - whether a value is free text or a constrained vocabulary
35
+ - which operations the product actually exposes
36
+ - the user-facing success and failure language
37
+
38
+ Those decisions come from the product conversation or existing application,
39
+ not from a JSKIT questionnaire.
40
+
41
+ ## Invariants
42
+
43
+ - The persisted table has a normal single-column primary key.
44
+ - User ownership is represented by a non-null `user_id` column.
45
+ - The resource's hidden `userId` field maps to that ownership column.
46
+ - `autofilter: "user"` is used only for genuine current-user ownership.
47
+ - Output, create, and patch participation is explicit on each product field.
48
+ - Temporal database values use the declared storage serializer.
49
+ - The source-controlled migration and resource contract agree.
50
+ - Server policy tests prove that one user cannot read or mutate another user's
51
+ records.
52
+
53
+ ## Framework APIs
54
+
55
+ The example uses `defineCrudResource()` from
56
+ `@jskit-ai/resource-crud-core/shared/crudResource`. That API derives the normal
57
+ CRUD operation validators from one readable field contract.
58
+
59
+ Higher layers should consume this resource through JSKIT's CRUD service,
60
+ repository, route, and UI APIs. They should not rebuild field validators or
61
+ serialize requests independently.
62
+
63
+ ## Example files
64
+
65
+ - `example/bookResource.js` is a complete resource contract for personal book
66
+ records.
67
+
68
+ The example deliberately uses a concrete domain. Rename and edit normal source
69
+ rather than feeding it through placeholder interpolation.
70
+
71
+ ## Variation points
72
+
73
+ Safe adaptations include:
74
+
75
+ - namespace and table name
76
+ - product fields and validation
77
+ - searchable fields and default sort
78
+ - enabled CRUD operations
79
+ - user-facing messages
80
+ - lookup relationships supported by the resource APIs
81
+
82
+ Changing ownership, access, identity, or persistence architecture selects a
83
+ different pattern and requires corresponding policy and migration evidence.
84
+
85
+ ## Verification
86
+
87
+ - Import the resource and prove all intended operations exist.
88
+ - Validate representative accepted and rejected field values.
89
+ - Rebuild the table from source-controlled migrations in a disposable database.
90
+ - Run positive current-user CRUD tests.
91
+ - Run negative cross-user read and mutation tests.
92
+ - Run the application's focused tests and broad verifier at sign-off.
93
+
94
+ ## Avoid
95
+
96
+ - generator provenance or scaffold-shape metadata
97
+ - a field-by-field questionnaire
98
+ - a live table as the only source of schema truth
99
+ - app-specific request serializers that duplicate the resource contract
100
+ - hidden ownership aliases other than the framework's explicit ownership field
101
+ - durable receipts recording that this pattern was copied
102
+ - changing an installed baseline migration instead of adding a new migration
103
+
@@ -0,0 +1,87 @@
1
+ import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
2
+
3
+ const bookResource = defineCrudResource({
4
+ namespace: "books",
5
+ tableName: "books",
6
+ apiAccess: "authenticated",
7
+ schema: {
8
+ userId: {
9
+ type: "id",
10
+ required: true,
11
+ hidden: true,
12
+ operations: {}
13
+ },
14
+ title: {
15
+ type: "string",
16
+ maxLength: 255,
17
+ required: true,
18
+ search: true,
19
+ operations: {
20
+ output: { required: true },
21
+ create: { required: true },
22
+ patch: { required: false }
23
+ }
24
+ },
25
+ author: {
26
+ type: "string",
27
+ maxLength: 255,
28
+ required: true,
29
+ search: true,
30
+ operations: {
31
+ output: { required: true },
32
+ create: { required: true },
33
+ patch: { required: false }
34
+ }
35
+ },
36
+ notes: {
37
+ type: "string",
38
+ maxLength: 65535,
39
+ nullable: true,
40
+ search: true,
41
+ operations: {
42
+ output: { required: true },
43
+ create: { required: false },
44
+ patch: { required: false }
45
+ }
46
+ },
47
+ createdAt: {
48
+ type: "dateTime",
49
+ temporalPrecision: 0,
50
+ default: "now()",
51
+ storage: { writeSerializer: "datetime-utc" },
52
+ operations: {
53
+ output: { required: true }
54
+ }
55
+ },
56
+ updatedAt: {
57
+ type: "dateTime",
58
+ temporalPrecision: 0,
59
+ default: "now()",
60
+ storage: { writeSerializer: "datetime-utc" },
61
+ operations: {
62
+ output: { required: true }
63
+ }
64
+ }
65
+ },
66
+ searchSchema: {
67
+ id: { type: "id", actualField: "id" },
68
+ q: {
69
+ type: "string",
70
+ oneOf: ["title", "author", "notes"],
71
+ filterOperator: "like",
72
+ splitBy: " ",
73
+ matchAll: true
74
+ }
75
+ },
76
+ defaultSort: ["-createdAt"],
77
+ autofilter: "user",
78
+ messages: {
79
+ validation: "Fix invalid values and try again.",
80
+ saveSuccess: "Book saved.",
81
+ saveError: "Unable to save this book.",
82
+ deleteSuccess: "Book deleted.",
83
+ deleteError: "Unable to delete this book."
84
+ }
85
+ });
86
+
87
+ export { bookResource };
@@ -0,0 +1,28 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { bookResource } from "../patterns/resource-contract/example/bookResource.js";
4
+
5
+ test("owner-scoped resource pattern is executable framework source", () => {
6
+ assert.equal(bookResource.namespace, "books");
7
+ assert.equal(bookResource.tableName, "books");
8
+ assert.equal(bookResource.apiAccess, "authenticated");
9
+ assert.equal(bookResource.autofilter, "user");
10
+ assert.deepEqual(
11
+ Object.keys(bookResource.operations),
12
+ ["list", "view", "create", "patch", "delete"]
13
+ );
14
+ assert.deepEqual(
15
+ bookResource.operations.create.body.schema.create({
16
+ title: "The Dispossessed",
17
+ author: "Ursula K. Le Guin",
18
+ notes: null
19
+ }).errors,
20
+ {}
21
+ );
22
+ assert.match(
23
+ bookResource.operations.create.body.schema.create({
24
+ author: "Ursula K. Le Guin"
25
+ }).errors.title.message,
26
+ /required/i
27
+ );
28
+ });