@antelopejs/interface-database-decorators 0.1.9 → 0.1.11

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.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/antelope.test.js +1 -1
  3. package/dist/common.d.ts +2 -1
  4. package/dist/common.js +5 -1
  5. package/dist/common.js.map +1 -1
  6. package/dist/database.js +1 -0
  7. package/dist/database.js.map +1 -1
  8. package/dist/model.js +29 -26
  9. package/dist/model.js.map +1 -1
  10. package/dist/modifiers/common.d.ts +24 -4
  11. package/dist/modifiers/common.js +92 -114
  12. package/dist/modifiers/common.js.map +1 -1
  13. package/dist/modifiers/encryption.js +14 -9
  14. package/dist/modifiers/encryption.js.map +1 -1
  15. package/dist/modifiers/internal/kinds.d.ts +12 -0
  16. package/dist/modifiers/internal/kinds.js +29 -0
  17. package/dist/modifiers/internal/kinds.js.map +1 -0
  18. package/dist/modifiers/internal/records.d.ts +29 -0
  19. package/dist/modifiers/internal/records.js +42 -0
  20. package/dist/modifiers/internal/records.js.map +1 -0
  21. package/dist/modifiers/localization.d.ts +19 -1
  22. package/dist/modifiers/localization.js +41 -6
  23. package/dist/modifiers/localization.js.map +1 -1
  24. package/dist/schema.js +1 -2
  25. package/dist/schema.js.map +1 -1
  26. package/dist/table.d.ts +8 -0
  27. package/dist/table.js +21 -6
  28. package/dist/table.js.map +1 -1
  29. package/dist/tests/codec_helpers.d.ts +6 -2
  30. package/dist/tests/codec_helpers.js +18 -14
  31. package/dist/tests/codec_helpers.js.map +1 -1
  32. package/dist/tests/common.test.js +38 -46
  33. package/dist/tests/common.test.js.map +1 -1
  34. package/dist/tests/cross_instance.test.js +27 -2
  35. package/dist/tests/cross_instance.test.js.map +1 -1
  36. package/dist/tests/database.test.js +247 -226
  37. package/dist/tests/database.test.js.map +1 -1
  38. package/dist/tests/helpers/other-instance.d.ts +1 -1
  39. package/dist/tests/helpers/other-instance.js +41 -9
  40. package/dist/tests/helpers/other-instance.js.map +1 -1
  41. package/dist/tests/integration.test.js +485 -469
  42. package/dist/tests/integration.test.js.map +1 -1
  43. package/dist/tests/model.test.js +293 -308
  44. package/dist/tests/model.test.js.map +1 -1
  45. package/dist/tests/modifiers/autodate.test.js +80 -86
  46. package/dist/tests/modifiers/autodate.test.js.map +1 -1
  47. package/dist/tests/modifiers/common.test.js +181 -195
  48. package/dist/tests/modifiers/common.test.js.map +1 -1
  49. package/dist/tests/modifiers/encryption.test.js +149 -155
  50. package/dist/tests/modifiers/encryption.test.js.map +1 -1
  51. package/dist/tests/modifiers/hash.test.js +91 -99
  52. package/dist/tests/modifiers/hash.test.js.map +1 -1
  53. package/dist/tests/modifiers/localization.test.js +186 -159
  54. package/dist/tests/modifiers/localization.test.js.map +1 -1
  55. package/dist/tests/relation.test.js +92 -97
  56. package/dist/tests/relation.test.js.map +1 -1
  57. package/dist/tests/schema.test.js +101 -106
  58. package/dist/tests/schema.test.js.map +1 -1
  59. package/dist/tests/table.test.js +124 -132
  60. package/dist/tests/table.test.js.map +1 -1
  61. package/docs/1.introduction.md +56 -2
  62. package/docs/2.table-definitions.md +15 -0
  63. package/docs/3.table-modifiers.md +10 -4
  64. package/docs/4.data-models.md +12 -0
  65. package/docs/5.parameter-decoration.md +4 -0
  66. package/docs/assets/model-conversion.svg +21 -0
  67. package/package.json +16 -5
  68. package/skills/database-decorators-interface/SKILL.md +4 -0
  69. package/skills/database-decorators-interface/cases.json +71 -0
@@ -95,6 +95,8 @@ class User extends Table.with(HashModifier) {
95
95
  | ----------- | -------- | ---------- | ------------------ |
96
96
  | `algorithm` | `string` | `"sha256"` | Hashing algorithm. |
97
97
 
98
+ `LocalizationModifier.fallbackLocaleOf(TableClass, field)` returns a field's `fallbackLocale`, or `undefined` when the field is not localized or declares none. Generated forms use it to require a value in that locale.
99
+
98
100
  #### Mixin Method
99
101
 
100
102
  The `HashModifier` mixin adds a `testHash` method to table instances:
@@ -175,9 +177,9 @@ translations.name = {
175
177
 
176
178
  #### Options
177
179
 
178
- | Option | Type | Description |
179
- | ---------------- | -------- | ------------------------------------------------------- |
180
- | `fallbackLocale` | `string` | Locale to use when the requested locale is unavailable. |
180
+ | Option | Type | Description |
181
+ | ---------------- | -------- | --------------------------------------------------------------------- |
182
+ | `fallbackLocale` | `string` | Locale to use when the requested locale has no value or an empty one. |
181
183
 
182
184
  #### Mixin Method
183
185
 
@@ -187,7 +189,11 @@ The `LocalizationModifier` mixin adds a `localize` method:
187
189
  localize(locale: string, fields?: Array<keyof this>): this
188
190
  ```
189
191
 
190
- Sets the active locale for accessing localized fields and returns the same, mutated object. It does not make a locale-specific copy: `spanish`, `german`, and `product` above all refer to the same object, and subsequent calls change what all those references read. If `fields` is provided, only those fields are unlocked for the specified locale. Copy the values you need before changing locales, and do not share one instance between concurrent locale-specific operations.
192
+ `localize()` changes the active locale and returns the same object, not a locale-specific copy.
193
+
194
+ - **References share state.** `spanish`, `german`, and `product` above refer to one object. A later `localize()` call changes what all three references read.
195
+ - **Fields can be restricted.** When you supply `fields`, only those fields are unlocked for the selected locale.
196
+ - **Copy values before switching.** Save the values you need before changing locales, and do not share one instance between concurrent locale-specific operations.
191
197
 
192
198
  ### AutoDateModifier
193
199
 
@@ -100,6 +100,16 @@ await userModel.delete("user-123");
100
100
 
101
101
  Data models include static methods for converting between different data representations:
102
102
 
103
+ | Starting value | Conversion | Result | Persists data? |
104
+ | ----------------------- | --------------- | ------------------------------------------------- | -------------- |
105
+ | Plain application input | `fromPlainData` | Table instance with input modifier events applied | No |
106
+ | Raw database record | `fromDatabase` | Table instance with read modifier events applied | No |
107
+ | Table instance | `toDatabase` | Plain storage representation | No |
108
+
109
+ Use `insert` or `update` to perform a write; conversion alone does not save anything. Reads through `get`, `getBy`, and `getAll` apply `fromDatabase` for you. A custom query through `model.table` returns AQL results, so apply conversion explicitly when you need model instances.
110
+
111
+ ![Model writes convert input, trigger insert or update events, convert to storage, then execute AQL. Model reads convert fetched records; direct model.table queries bypass model conversion.](./assets/model-conversion.svg){width="480"}
112
+
103
113
  ### `fromPlainData(obj)`
104
114
 
105
115
  Convert a plain JavaScript object into a Table class instance. Modifier transformations (like encryption and hashing) are applied during this conversion.
@@ -152,6 +162,8 @@ await userModel.update("user-123", { name: "Bob" }, { validate: true });
152
162
 
153
163
  Extend the generated model class to add custom query methods:
154
164
 
165
+ The `register` method below checks for an existing email to provide early feedback. It does not guarantee uniqueness under concurrent writes; enforce that constraint in the database where supported and handle conflicts.
166
+
155
167
  ```typescript
156
168
  const UserModelBase = BasicDataModel(User, "users");
157
169
 
@@ -29,6 +29,8 @@ class UsersController extends Controller("/users") {
29
29
 
30
30
  Pass a callback function to determine the instance ID at request time. The callback receives the request context:
31
31
 
32
+ Authorize access to the requested tenant before reading or writing through the injected model. The callback selects an instance; a URL parameter alone is not proof that the caller belongs to that tenant.
33
+
32
34
  ```typescript
33
35
  import { Controller, Get, Parameter } from "@antelopejs/interface-api";
34
36
  import { Model } from "@antelopejs/interface-database-decorators";
@@ -90,6 +92,8 @@ const model3 = GetModel(UserModel, "other");
90
92
 
91
93
  Model decorators compose naturally with other API decorators for building complete CRUD controllers:
92
94
 
95
+ This example demonstrates injection, not a complete public API policy. Before exposing writes, reject malformed or non-object JSON, allow only permitted fields, validate their values, and authorize the operation. `JSON.parse` and `@Model` do not provide these checks; string-token `@Field` declarations do not validate input either.
96
+
93
97
  ```typescript
94
98
  import {
95
99
  Controller,
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 460" role="img" aria-labelledby="title desc">
2
+ <title id="title">Model conversion and database execution</title>
3
+ <desc id="desc">Writing through a model converts plain input, triggers insert or update modifier events, converts to storage, and executes an AQL write. Reading through a model converts a fetched record with fromDatabase. Static conversion functions do not persist data, and model.table returns raw AQL results.</desc>
4
+ <rect width="480" height="460" rx="16" fill="#f5f7fb"/>
5
+ <g font-family="system-ui, sans-serif" fill="#172b45">
6
+ <text x="24" y="36" font-size="20" font-weight="700">Conversion is not persistence</text>
7
+ <rect x="24" y="58" width="432" height="160" rx="10" fill="#e1ecfa"/>
8
+ <text x="42" y="87" font-size="18" font-weight="700">Model write · insert / update</text>
9
+ <text x="42" y="118" font-size="16">Input → fromPlainData</text>
10
+ <text x="42" y="145" font-size="16">→ insert / update modifier events</text>
11
+ <text x="42" y="172" font-size="16">→ toDatabase → storage representation</text>
12
+ <text x="42" y="199" font-size="16">→ AQL write → IDs / modified count</text>
13
+ <rect x="24" y="238" width="432" height="100" rx="10" fill="#d9eee8"/>
14
+ <text x="42" y="267" font-size="18" font-weight="700">Model read · get / getBy / getAll</text>
15
+ <text x="42" y="296" font-size="16">AQL read → fetched record</text>
16
+ <text x="42" y="323" font-size="16">→ fromDatabase → table instance</text>
17
+ <text x="24" y="375" font-size="16" font-weight="700">Taking the direct AQL path?</text>
18
+ <text x="24" y="403" font-size="16">model.table builds queries without model conversion.</text>
19
+ <text x="24" y="431" font-size="16">Apply fromDatabase to raw rows when needed.</text>
20
+ </g>
21
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/interface-database-decorators",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "AntelopeJS decorators for database tables and typed data models, with encrypted, hashed and localized fields",
5
5
  "keywords": [
6
6
  "antelopejs",
@@ -127,7 +127,17 @@
127
127
  "release": "pnpm run lint && pnpm run prepack && release-it",
128
128
  "test": "pnpm run build && ajs module test .",
129
129
  "format:check": "oxfmt --check .",
130
- "knip": "knip"
130
+ "knip": "knip",
131
+ "prepare": "if git rev-parse --is-inside-work-tree >/dev/null 2>&1 && command -v lefthook >/dev/null 2>&1; then lefthook install; fi",
132
+ "checks": "antelopejs-checks",
133
+ "size": "antelopejs-check-size",
134
+ "ratchet": "antelopejs-ratchet",
135
+ "sync": "antelopejs-sync",
136
+ "ratchet:update": "antelopejs-ratchet --update",
137
+ "sync:check": "antelopejs-sync --check",
138
+ "typecheck": "tsc --noEmit",
139
+ "suppressions": "antelopejs-suppressions",
140
+ "suppressions:update": "antelopejs-suppressions --update"
131
141
  },
132
142
  "dependencies": {
133
143
  "randomstring": "^1.3.1",
@@ -137,8 +147,8 @@
137
147
  "@antelopejs/core": "^1.16.0",
138
148
  "@antelopejs/interface-api": "^0.0.15",
139
149
  "@antelopejs/interface-core": "^0.2.4",
140
- "@antelopejs/interface-database": "^0.1.10",
141
- "@antelopejs/tooling-configs": "^0.0.9",
150
+ "@antelopejs/interface-database": "^0.1.11",
151
+ "@antelopejs/tooling-configs": "^0.2.11",
142
152
  "@types/chai": "^4.3.20",
143
153
  "@types/mocha": "^10.0.10",
144
154
  "@types/node": "^22.19.15",
@@ -147,6 +157,7 @@
147
157
  "chai": "^4.5.0",
148
158
  "eslint-plugin-perfectionist": "^5.11.0",
149
159
  "knip": "^6.34.0",
160
+ "lefthook": "^2.2.2",
150
161
  "mongodb-memory-server-core": "^10.4.3",
151
162
  "oxfmt": "^0.66.0",
152
163
  "oxlint": "1.81.0",
@@ -159,7 +170,7 @@
159
170
  "peerDependencies": {
160
171
  "@antelopejs/interface-api": ">=0.0.15 <1.0.0",
161
172
  "@antelopejs/interface-core": ">=0.2.4 <1.0.0",
162
- "@antelopejs/interface-database": ">=0.1.10 <1.0.0"
173
+ "@antelopejs/interface-database": ">=0.1.11 <1.0.0"
163
174
  },
164
175
  "packageManager": "pnpm@10.6.5",
165
176
  "antelopeJs": {
@@ -127,6 +127,7 @@ class Chef extends Table.with(
127
127
  - Hashed fields cannot be read back or queried directly — only equality-tested with `testHash`.
128
128
  - Localized fields must be written through a localized instance (`instance.localize(locale).field = ...`);
129
129
  values assigned before `localize()` are held in floating state and silently dropped on insert/update.
130
+ - A localized value `""` is stored as no value: reads and queries in that locale get the `fallbackLocale` value.
130
131
  - `@Relation({ to: () => Target })` is declarative metadata only: no foreign-key enforcement, consumed by introspection tooling via `RelationStaticMetadata`.
131
132
  - `Model.validate` (and `{ validate: true }` on insert/update) only checks fields whose `@Field` type is an
132
133
  io-ts-style codec with `.decode`; plain string field tokens are skipped. Update validates with
@@ -135,6 +136,9 @@ class Chef extends Table.with(
135
136
  - Indexes are scoped to the schema instance. Add `@Index({ crossInstance: true })` only for indexes also queried
136
137
  across all instances (`CROSS_INSTANCE`); it never changes results, only speed. On a group, one member setting it
137
138
  applies it to the whole group.
139
+ - `@Index({ unique: true })` refuses two rows of an instance with the same key: the write throws `DuplicateKeyError`
140
+ (test with `IsDuplicateKeyError` from `@antelopejs/interface-database`). On a group, one member setting it applies it
141
+ to the whole group.
138
142
  - `@Fixture` data is only inserted by `RegisterSchema` when the table is empty (count === 0).
139
143
  - `GetModel` caches by model class + instanceId; the same pair always returns the same instance.
140
144
  - `RegisterSchema` binds each registered table class to the `Schema` it builds; call it in the module that declares the tables. `User.table()`, `User.table(instanceId)` and `User.table(db)` then return the class's AQL table, and `GetModel` builds `BasicDataModel` models on that same schema, even when another module (bound to another database) calls them. An unbound class throws rather than looking a schema up by id.
@@ -0,0 +1,71 @@
1
+ {
2
+ "version": 1,
3
+ "cases": [
4
+ {
5
+ "id": "table-relations",
6
+ "area": "framework",
7
+ "style": "clean",
8
+ "prompt": "How do I define relations between tables?",
9
+ "origin": "docs-assistant",
10
+ "expect": {
11
+ "skills": ["database-decorators-interface"],
12
+ "acceptable": ["database-interface"],
13
+ "mustMention": ["Relation"]
14
+ }
15
+ },
16
+ {
17
+ "id": "table-relations-casual",
18
+ "area": "framework",
19
+ "style": "casual",
20
+ "prompt": "how do i link two tables together",
21
+ "origin": "docs-assistant",
22
+ "expect": {
23
+ "skills": ["database-decorators-interface"],
24
+ "acceptable": ["database-interface"],
25
+ "mustMention": ["Relation"]
26
+ }
27
+ },
28
+ {
29
+ "id": "database-decorators-interface-aql-neighbour",
30
+ "area": "framework",
31
+ "style": "clean",
32
+ "prompt": "Write a query that returns the novels with at least 300 pages, sorted by page count descending, and watch the table for changes.",
33
+ "expect": {
34
+ "skills": ["database-interface"],
35
+ "acceptable": ["antelopejs", "ajs-consume-interface"],
36
+ "avoid": ["database-decorators-interface"]
37
+ }
38
+ },
39
+ {
40
+ "id": "database-decorators-interface-modifiers",
41
+ "area": "framework",
42
+ "style": "clean",
43
+ "prompt": "On my user table class, store the password so it can only be verified, encrypt the API key transparently, and keep created and updated timestamps.",
44
+ "expect": {
45
+ "skills": ["database-decorators-interface"],
46
+ "acceptable": [
47
+ "database-interface",
48
+ "antelopejs",
49
+ "ajs-consume-interface"
50
+ ],
51
+ "mustMention": [
52
+ "Hashed",
53
+ "Encrypted",
54
+ "HashModifier",
55
+ "EncryptionModifier"
56
+ ]
57
+ }
58
+ },
59
+ {
60
+ "id": "database-decorators-interface-fixture-casual",
61
+ "area": "framework",
62
+ "style": "casual",
63
+ "prompt": "how do i seed some default rows when the table is empty",
64
+ "expect": {
65
+ "skills": ["database-decorators-interface"],
66
+ "acceptable": ["database-interface", "antelopejs"],
67
+ "mustMention": ["Fixture", "RegisterSchema"]
68
+ }
69
+ }
70
+ ]
71
+ }