@antelopejs/interface-database 0.1.4 → 0.1.5
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/dist/antelope.test.js +2 -2
- package/dist/antelope.test.js.map +1 -1
- package/dist/atomic.d.ts +41 -0
- package/dist/atomic.js +135 -0
- package/dist/atomic.js.map +1 -0
- package/dist/common.js.map +1 -1
- package/dist/datum.d.ts +2 -2
- package/dist/datum.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/query.js +1 -1
- package/dist/query.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/selection.d.ts +4 -1
- package/dist/selection.js +7 -0
- package/dist/selection.js.map +1 -1
- package/dist/stream.d.ts +1 -1
- package/dist/stream.js.map +1 -1
- package/dist/tests/aggregation_operations.test.js +1 -1
- package/dist/tests/aggregation_operations.test.js.map +1 -1
- package/dist/tests/atomic_mutation.test.d.ts +1 -0
- package/dist/tests/atomic_mutation.test.js +185 -0
- package/dist/tests/atomic_mutation.test.js.map +1 -0
- package/dist/tests/basic_operations.test.js +1 -1
- package/dist/tests/basic_operations.test.js.map +1 -1
- package/dist/tests/between_operations.test.js +1 -1
- package/dist/tests/between_operations.test.js.map +1 -1
- package/dist/tests/change_feeds.test.js +1 -1
- package/dist/tests/change_feeds.test.js.map +1 -1
- package/dist/tests/datasets/codec_fields.js.map +1 -1
- package/dist/tests/date_operations.test.js +1 -1
- package/dist/tests/date_operations.test.js.map +1 -1
- package/dist/tests/do_operations.test.js +1 -1
- package/dist/tests/do_operations.test.js.map +1 -1
- package/dist/tests/filters.test.js +1 -1
- package/dist/tests/filters.test.js.map +1 -1
- package/dist/tests/group_operations.test.js +1 -1
- package/dist/tests/group_operations.test.js.map +1 -1
- package/dist/tests/instance_modes.test.js +1 -1
- package/dist/tests/instance_modes.test.js.map +1 -1
- package/dist/tests/joins.test.js +2 -2
- package/dist/tests/joins.test.js.map +1 -1
- package/dist/tests/lookup_operations.test.js +2 -2
- package/dist/tests/lookup_operations.test.js.map +1 -1
- package/dist/tests/merge_operations.test.js +2 -2
- package/dist/tests/merge_operations.test.js.map +1 -1
- package/dist/tests/sorting.test.js +1 -1
- package/dist/tests/sorting.test.js.map +1 -1
- package/dist/tests/union_operations.test.js +2 -2
- package/dist/tests/union_operations.test.js.map +1 -1
- package/docs/1.query_types/2.table.md +10 -4
- package/docs/1.query_types/3.selection.md +15 -17
- package/docs/1.query_types/5.stream.md +2 -5
- package/docs/1.query_types/6.feed.md +1 -1
- package/docs/1.query_types/9.valueproxy.md +43 -41
- package/docs/2.operations/2.database_management.md +2 -2
- package/docs/2.operations/3.table_operations.md +1 -1
- package/docs/2.operations/4.crud.md +1 -2
- package/docs/2.operations/5.indexes.md +4 -4
- package/docs/2.operations/6.filtering.md +23 -27
- package/docs/2.operations/7.lookup.md +13 -15
- package/docs/2.operations/8.atomic_mutations.md +71 -0
- package/docs/3.results/3.index_changes.md +9 -5
- package/docs/3.results/4.table_changes.md +3 -3
- package/docs/3.results/6.value_changes.md +5 -5
- package/docs/index.md +1 -0
- package/package.json +55 -40
- package/skills/database-interface/SKILL.md +41 -7
|
@@ -44,7 +44,10 @@ Logical operators for combining boolean expressions.
|
|
|
44
44
|
|
|
45
45
|
```typescript
|
|
46
46
|
const canAccess = user.key("active").and(user.key("verified"));
|
|
47
|
-
const isSpecial = user
|
|
47
|
+
const isSpecial = user
|
|
48
|
+
.key("role")
|
|
49
|
+
.eq("admin")
|
|
50
|
+
.or(user.key("plan").eq("premium"));
|
|
48
51
|
const isInactive = user.key("active").not();
|
|
49
52
|
```
|
|
50
53
|
|
|
@@ -97,12 +100,12 @@ const isRecent = date.ge(new Date("2024-01-01"));
|
|
|
97
100
|
const isBefore = date.lt(deadline);
|
|
98
101
|
```
|
|
99
102
|
|
|
100
|
-
| Method
|
|
101
|
-
|
|
|
102
|
-
| `gt(v)`
|
|
103
|
-
| `ge(v)`
|
|
104
|
-
| `lt(v)`
|
|
105
|
-
| `le(v)`
|
|
103
|
+
| Method | Description |
|
|
104
|
+
| ------- | --------------------- |
|
|
105
|
+
| `gt(v)` | Greater than |
|
|
106
|
+
| `ge(v)` | Greater than or equal |
|
|
107
|
+
| `lt(v)` | Less than |
|
|
108
|
+
| `le(v)` | Less than or equal |
|
|
106
109
|
|
|
107
110
|
## Date Operations
|
|
108
111
|
|
|
@@ -127,7 +130,7 @@ const h = created.hours();
|
|
|
127
130
|
const min = created.minutes();
|
|
128
131
|
const sec = created.seconds();
|
|
129
132
|
const tod = created.timeofday(); // seconds since midnight
|
|
130
|
-
const epoch = created.epoch();
|
|
133
|
+
const epoch = created.epoch(); // seconds since UNIX epoch
|
|
131
134
|
```
|
|
132
135
|
|
|
133
136
|
### Range Check
|
|
@@ -152,14 +155,14 @@ const len = name.strlen();
|
|
|
152
155
|
const matches = email.match("^.*@gmail\\.com$");
|
|
153
156
|
```
|
|
154
157
|
|
|
155
|
-
| Method
|
|
156
|
-
|
|
|
157
|
-
| `upcase()`
|
|
158
|
-
| `downcase()`
|
|
159
|
-
| `split(sep, max?)`
|
|
160
|
-
| `concat(other)`
|
|
161
|
-
| `strlen()`
|
|
162
|
-
| `match(regex)`
|
|
158
|
+
| Method | Description |
|
|
159
|
+
| ------------------ | ------------------------------- |
|
|
160
|
+
| `upcase()` | Convert to uppercase |
|
|
161
|
+
| `downcase()` | Convert to lowercase |
|
|
162
|
+
| `split(sep, max?)` | Split by separator |
|
|
163
|
+
| `concat(other)` | Concatenate with another string |
|
|
164
|
+
| `strlen()` | Number of Unicode codepoints |
|
|
165
|
+
| `match(regex)` | Test against a regex pattern |
|
|
163
166
|
|
|
164
167
|
## Array Operations
|
|
165
168
|
|
|
@@ -179,19 +182,19 @@ const lowest = scores.min();
|
|
|
179
182
|
const highest = scores.max();
|
|
180
183
|
```
|
|
181
184
|
|
|
182
|
-
| Method
|
|
183
|
-
|
|
|
184
|
-
| `index(n)`
|
|
185
|
-
| `includes(val)`
|
|
186
|
-
| `slice(s, e?)`
|
|
187
|
-
| `map(fn)`
|
|
188
|
-
| `filter(fn)`
|
|
189
|
-
| `isempty()`
|
|
190
|
-
| `count()`
|
|
191
|
-
| `sum()`
|
|
192
|
-
| `avg()`
|
|
193
|
-
| `min()`
|
|
194
|
-
| `max()`
|
|
185
|
+
| Method | Description |
|
|
186
|
+
| --------------- | ----------------------------------- |
|
|
187
|
+
| `index(n)` | Element at position `n` |
|
|
188
|
+
| `includes(val)` | Check if the array contains a value |
|
|
189
|
+
| `slice(s, e?)` | Extract a sub-array |
|
|
190
|
+
| `map(fn)` | Transform each element |
|
|
191
|
+
| `filter(fn)` | Keep elements matching a predicate |
|
|
192
|
+
| `isempty()` | Check if the array is empty |
|
|
193
|
+
| `count()` | Number of elements |
|
|
194
|
+
| `sum()` | Sum of numeric elements |
|
|
195
|
+
| `avg()` | Average of numeric elements |
|
|
196
|
+
| `min()` | Minimum of numeric elements |
|
|
197
|
+
| `max()` | Maximum of numeric elements |
|
|
195
198
|
|
|
196
199
|
## Object Operations
|
|
197
200
|
|
|
@@ -206,13 +209,13 @@ const fieldValues = user.values();
|
|
|
206
209
|
const hasRequired = user.hasfields("email", "name");
|
|
207
210
|
```
|
|
208
211
|
|
|
209
|
-
| Method | Description
|
|
210
|
-
| ------------------ |
|
|
211
|
-
| `key(k, def?)` | Access a field, with optional default|
|
|
212
|
-
| `merge(other)` | Merge with another object
|
|
213
|
-
| `keys()` | Array of field names
|
|
214
|
-
| `values()` | Array of field values
|
|
215
|
-
| `hasfields(...fs)` | Check if all specified fields exist
|
|
212
|
+
| Method | Description |
|
|
213
|
+
| ------------------ | ------------------------------------- |
|
|
214
|
+
| `key(k, def?)` | Access a field, with optional default |
|
|
215
|
+
| `merge(other)` | Merge with another object |
|
|
216
|
+
| `keys()` | Array of field names |
|
|
217
|
+
| `values()` | Array of field values |
|
|
218
|
+
| `hasfields(...fs)` | Check if all specified fields exist |
|
|
216
219
|
|
|
217
220
|
## Compose Expressions
|
|
218
221
|
|
|
@@ -220,11 +223,10 @@ ValueProxy methods are chainable, enabling complex expressions within query call
|
|
|
220
223
|
|
|
221
224
|
```typescript
|
|
222
225
|
const result = await users.filter((user) =>
|
|
223
|
-
user
|
|
226
|
+
user
|
|
227
|
+
.key("age")
|
|
228
|
+
.ge(18)
|
|
224
229
|
.and(user.key("status").eq("active"))
|
|
225
|
-
.and(
|
|
226
|
-
user.key("role").eq("admin")
|
|
227
|
-
.or(user.key("plan").eq("premium")),
|
|
228
|
-
),
|
|
230
|
+
.and(user.key("role").eq("admin").or(user.key("plan").eq("premium"))),
|
|
229
231
|
);
|
|
230
232
|
```
|
|
@@ -37,8 +37,8 @@ interface TableDefinition {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
interface IndexDefinition {
|
|
40
|
-
fields?: string[];
|
|
41
|
-
multi?: boolean;
|
|
40
|
+
fields?: string[]; // For compound indexes
|
|
41
|
+
multi?: boolean; // Multi-valued index
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
@@ -30,7 +30,7 @@ const users = db.table<User>("users");
|
|
|
30
30
|
|
|
31
31
|
// TypeScript now validates field names and types
|
|
32
32
|
const user = await users.get("user-123"); // user is User | null
|
|
33
|
-
const emails = await users.key("email");
|
|
33
|
+
const emails = await users.key("email"); // string[]
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## Basic Operations
|
|
@@ -89,8 +89,7 @@ const recent = await users.between(
|
|
|
89
89
|
```typescript
|
|
90
90
|
// Filter with a predicate function
|
|
91
91
|
const active = await users.filter((user) =>
|
|
92
|
-
user.key("status").eq("active")
|
|
93
|
-
.and(user.key("age").ge(18)),
|
|
92
|
+
user.key("status").eq("active").and(user.key("age").ge(18)),
|
|
94
93
|
);
|
|
95
94
|
```
|
|
96
95
|
|
|
@@ -13,8 +13,8 @@ const schema = new Schema("myapp", {
|
|
|
13
13
|
users: {
|
|
14
14
|
fields: { name: "string", email: "string", role: "string", age: "number" },
|
|
15
15
|
indexes: {
|
|
16
|
-
email: {},
|
|
17
|
-
role: {},
|
|
16
|
+
email: {}, // Simple index on the "email" field
|
|
17
|
+
role: {}, // Simple index on the "role" field
|
|
18
18
|
name_age: { fields: ["name", "age"] }, // Compound index
|
|
19
19
|
},
|
|
20
20
|
},
|
|
@@ -25,8 +25,8 @@ const schema = new Schema("myapp", {
|
|
|
25
25
|
|
|
26
26
|
```typescript
|
|
27
27
|
interface IndexDefinition {
|
|
28
|
-
fields?: string[];
|
|
29
|
-
multi?: boolean;
|
|
28
|
+
fields?: string[]; // Fields for compound indexes (omit for single-field)
|
|
29
|
+
multi?: boolean; // Whether this is a multi-valued index
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -14,9 +14,7 @@ const users = schema.instance().table("users");
|
|
|
14
14
|
Use the `filter` method with a predicate function. The callback receives a `ValueProxy<T>` representing each document.
|
|
15
15
|
|
|
16
16
|
```typescript
|
|
17
|
-
const active = await users.filter((user) =>
|
|
18
|
-
user.key("status").eq("active"),
|
|
19
|
-
);
|
|
17
|
+
const active = await users.filter((user) => user.key("status").eq("active"));
|
|
20
18
|
```
|
|
21
19
|
|
|
22
20
|
## Comparison Operators
|
|
@@ -53,17 +51,14 @@ const important = await users.filter((u) =>
|
|
|
53
51
|
);
|
|
54
52
|
|
|
55
53
|
// NOT
|
|
56
|
-
const standard = await users.filter((u) =>
|
|
57
|
-
u.key("plan").eq("premium").not(),
|
|
58
|
-
);
|
|
54
|
+
const standard = await users.filter((u) => u.key("plan").eq("premium").not());
|
|
59
55
|
|
|
60
56
|
// Complex combinations
|
|
61
57
|
const target = await users.filter((u) =>
|
|
62
|
-
u
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
),
|
|
58
|
+
u
|
|
59
|
+
.key("status")
|
|
60
|
+
.eq("active")
|
|
61
|
+
.and(u.key("plan").eq("premium").or(u.key("totalPurchases").gt(500))),
|
|
67
62
|
);
|
|
68
63
|
```
|
|
69
64
|
|
|
@@ -71,9 +66,7 @@ const target = await users.filter((u) =>
|
|
|
71
66
|
|
|
72
67
|
```typescript
|
|
73
68
|
// Case-insensitive match
|
|
74
|
-
const bobs = await users.filter((u) =>
|
|
75
|
-
u.key("name").downcase().eq("bob"),
|
|
76
|
-
);
|
|
69
|
+
const bobs = await users.filter((u) => u.key("name").downcase().eq("bob"));
|
|
77
70
|
|
|
78
71
|
// Regex matching
|
|
79
72
|
const gmailUsers = await users.filter((u) =>
|
|
@@ -96,7 +89,9 @@ const newUsers = await users.filter((u) =>
|
|
|
96
89
|
);
|
|
97
90
|
|
|
98
91
|
const q1Users = await users.filter((u) =>
|
|
99
|
-
u
|
|
92
|
+
u
|
|
93
|
+
.key("createdAt")
|
|
94
|
+
.ge(new Date("2024-01-01"))
|
|
100
95
|
.and(u.key("createdAt").lt(new Date("2024-04-01"))),
|
|
101
96
|
);
|
|
102
97
|
```
|
|
@@ -104,9 +99,7 @@ const q1Users = await users.filter((u) =>
|
|
|
104
99
|
### Date Components
|
|
105
100
|
|
|
106
101
|
```typescript
|
|
107
|
-
const thisYear = await users.filter((u) =>
|
|
108
|
-
u.key("createdAt").year().eq(2024),
|
|
109
|
-
);
|
|
102
|
+
const thisYear = await users.filter((u) => u.key("createdAt").year().eq(2024));
|
|
110
103
|
|
|
111
104
|
const morningLogins = await users.filter((u) =>
|
|
112
105
|
u.key("lastLogin").hours().lt(12),
|
|
@@ -117,19 +110,22 @@ const morningLogins = await users.filter((u) =>
|
|
|
117
110
|
|
|
118
111
|
```typescript
|
|
119
112
|
// Check array contents
|
|
120
|
-
const jsPosts = await schema
|
|
121
|
-
|
|
122
|
-
)
|
|
113
|
+
const jsPosts = await schema
|
|
114
|
+
.instance()
|
|
115
|
+
.table("posts")
|
|
116
|
+
.filter((post) => post.key("tags").includes("javascript"));
|
|
123
117
|
|
|
124
118
|
// Check array length
|
|
125
|
-
const wellTagged = await schema
|
|
126
|
-
|
|
127
|
-
)
|
|
119
|
+
const wellTagged = await schema
|
|
120
|
+
.instance()
|
|
121
|
+
.table("posts")
|
|
122
|
+
.filter((post) => post.key("tags").count().gt(3));
|
|
128
123
|
|
|
129
124
|
// Check if empty
|
|
130
|
-
const untagged = await schema
|
|
131
|
-
|
|
132
|
-
)
|
|
125
|
+
const untagged = await schema
|
|
126
|
+
.instance()
|
|
127
|
+
.table("posts")
|
|
128
|
+
.filter((post) => post.key("tags").isempty());
|
|
133
129
|
```
|
|
134
130
|
|
|
135
131
|
## Default Values in Filters
|
|
@@ -4,11 +4,11 @@ The `lookup` method performs a foreign key join, replacing a field in your docum
|
|
|
4
4
|
|
|
5
5
|
## Parameters
|
|
6
6
|
|
|
7
|
-
| Parameter | Type | Description
|
|
8
|
-
| ---------- | -------------- |
|
|
9
|
-
| `other` | `Selection<U>` | The table containing the referenced documents
|
|
7
|
+
| Parameter | Type | Description |
|
|
8
|
+
| ---------- | -------------- | --------------------------------------------------------- |
|
|
9
|
+
| `other` | `Selection<U>` | The table containing the referenced documents |
|
|
10
10
|
| `localKey` | `keyof T` | The field in the current document holding the foreign key |
|
|
11
|
-
| `otherKey` | `keyof U` | The field in the other table to match against
|
|
11
|
+
| `otherKey` | `keyof U` | The field in the other table to match against |
|
|
12
12
|
|
|
13
13
|
## Behavior
|
|
14
14
|
|
|
@@ -122,16 +122,14 @@ const withLookup = await db
|
|
|
122
122
|
// order.customerId is now the full customer object
|
|
123
123
|
|
|
124
124
|
// Join: custom output structure
|
|
125
|
-
const withJoin = await db
|
|
126
|
-
.table("
|
|
127
|
-
.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
125
|
+
const withJoin = await db.table("orders").joinInner(
|
|
126
|
+
db.table("customers"),
|
|
127
|
+
(order, customer) => order.key("customerId").eq(customer.key("_id")),
|
|
128
|
+
(order, customer) => ({
|
|
129
|
+
orderId: order.key("_id"),
|
|
130
|
+
orderTotal: order.key("total"),
|
|
131
|
+
customerName: customer.key("name"),
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
136
134
|
// Result is a custom object with selected fields from both tables
|
|
137
135
|
```
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Atomic single-record mutations
|
|
2
|
+
|
|
3
|
+
`Table.atomicMutation(id, request)` checks one immutable, instance-scoped record identity and one condition inside the database mutation. It returns `Query<AtomicMutationOutcome>`. It does not provide multi-record transactions, a lock service, receipts, or external-effect coordination.
|
|
4
|
+
|
|
5
|
+
Generic `filter(...).update(...)` and `filter(...).delete()` do **not** promise this guarantee. Use a direct `schema.instance(instanceId).table(tableId)` with one literal string identity. The default instance is supported; `CROSS_INSTANCE`, filtered selections, expression keys, cursors, and upserts are not.
|
|
6
|
+
|
|
7
|
+
## Compare a revision
|
|
8
|
+
|
|
9
|
+
Public types are exported from `@antelopejs/interface-database` and `@antelopejs/interface-database/atomic`. An update checks the current revision and installs the patch and next revision together. A delete checks the revision inside the deletion.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
const outcome = await table.atomicMutation(recordId, {
|
|
13
|
+
type: "update",
|
|
14
|
+
revisionField: "revision",
|
|
15
|
+
expectedRevision: observed.revision,
|
|
16
|
+
nextRevision: freshRevision,
|
|
17
|
+
patch: { title: "Updated title" },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const deletion = await table.atomicMutation(recordId, {
|
|
21
|
+
type: "delete",
|
|
22
|
+
revisionField: "revision",
|
|
23
|
+
expectedRevision: observed.revision,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Revision tokens are nonempty strings. Each update must install a changed token that the caller guarantees has never been used for that identity. All writers of protected state must advance the revision; otherwise a successful comparison does not prove that the observed state is current. Initialize new records with fresh revisions and use a new identity for each incarnation, including recreation after deletion.
|
|
28
|
+
|
|
29
|
+
`expectedRevision: { kind: "missing" }` matches an **absent revision field on an existing record**. It does not match a stored `null`, an empty string, or a missing record. An update can use this expectation to bootstrap legacy data atomically; a delete can use it when the domain permits deleting an unversioned record. A backend that cannot distinguish absence from null must reject this expectation rather than weaken it. In PostgreSQL, declared SQL columns collapse this distinction; an overflow JSON field can preserve it. Review mapper serialization before choosing revision storage.
|
|
30
|
+
|
|
31
|
+
## Patch semantics
|
|
32
|
+
|
|
33
|
+
The patch replaces each supplied top-level field. Nested objects and arrays replace their entire previous values; they do not merge recursively. Unspecified fields remain unchanged, and `null` stores null rather than removing the field. There is no unset operation.
|
|
34
|
+
|
|
35
|
+
Patches contain only constant strings, booleans, finite numbers, null, valid Dates, dense arrays, and plain objects. Undefined, functions, query proxies, cycles, and operator/path field names are rejected. The patch cannot contain `id`, `_id`, the revision field, or adapter-owned scope fields. Adapters must preserve their normal Date storage representation without interpreting objects that resemble query stages as executable expressions.
|
|
36
|
+
|
|
37
|
+
## Delete by one observed scalar
|
|
38
|
+
|
|
39
|
+
`deleteIfEqual` provides a narrower condition for domains where one field equality is sufficient, such as deletion of an expired record whose observed timestamp remains unchanged.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const outcome = await table.atomicMutation(recordId, {
|
|
43
|
+
type: "deleteIfEqual",
|
|
44
|
+
field: "lastSeenAt",
|
|
45
|
+
expectedValue: observed.lastSeenAt,
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`expectedValue` accepts a string, finite number, boolean, or valid Date. Equality is exact and type-sensitive, with Dates compared by their stored instant; scalar equality must not match an array containing that scalar. Missing fields never match. Null, objects, arrays, identity fields, and scope fields are not supported.
|
|
50
|
+
|
|
51
|
+
This variant does not check or advance a revision and does not prevent ABA: a value can change and later return to its observed value. The domain must prove that equality alone authorizes deletion. It is not a substitute for revision checks on state transitions.
|
|
52
|
+
|
|
53
|
+
## Handle all outcomes
|
|
54
|
+
|
|
55
|
+
| Outcome | Meaning |
|
|
56
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| `applied` | The adapter received definitive acknowledgement that the one mutation applied. |
|
|
58
|
+
| `not-applied` | The adapter received definitive acknowledgement that the target was missing or its scope or condition did not match. |
|
|
59
|
+
| `unknown` | The adapter cannot determine whether the mutation applied, including acknowledgement loss. |
|
|
60
|
+
|
|
61
|
+
Definitive validation and unsupported-operation errors throw. Neither an exception nor `unknown` means mismatch. The interface rejects invalid adapter results, including `undefined`, zero, and generic modified counts, with `AtomicMutationUnsupportedError`; adapters must reject unsupported stages explicitly. There is no fallback to generic filtered writes and no separate capability probe. Use an adapter whose implementation has been verified before relying on this API.
|
|
62
|
+
|
|
63
|
+
The interface and adapter must not automatically retry an uncertain mutation. Do not repeat external effects or infer failure from a later mismatch. Domain-specific recovery can reconcile durable evidence and receipts, but a fresh read alone cannot prove whether an earlier operation or external effect occurred. Any deliberate replay requires a domain proof and does not turn `not-applied` into evidence about the original attempt.
|
|
64
|
+
|
|
65
|
+
## Adapter contract
|
|
66
|
+
|
|
67
|
+
The canonical terminal stage is `{ stage: "atomicMutation", options: undefined, args: [id, request] }`, immediately after `schema`, `instance`, and `table`. `AtomicMutation<T>` is the union of `AtomicUpdate<T>`, `AtomicDelete<T>`, and `AtomicDeleteIfEqual<T>`.
|
|
68
|
+
|
|
69
|
+
Adapters can reuse `ValidateAtomicMutation(id, request, protectedFields)` and `ValidateAtomicMutationTable(prefixStages)`. They must additionally validate the terminal envelope and schema-specific restrictions. Scope, immutable identity, and the revision or equality condition must participate in the target-local native mutation, not only an earlier selection or identity subquery. Backend tests must prove contention, scope isolation, missing/null distinction, top-level replacement, and acknowledgement-loss behavior.
|
|
70
|
+
|
|
71
|
+
This API remains unpublished during adapter and consumer verification. Package metadata alone does not establish capability; do not infer support from the unchanged development package version.
|
|
@@ -6,8 +6,8 @@ In AQL, indexes are defined as part of the schema definition rather than created
|
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
interface IndexDefinition {
|
|
9
|
-
fields?: string[];
|
|
10
|
-
multi?: boolean;
|
|
9
|
+
fields?: string[]; // Fields for compound indexes
|
|
10
|
+
multi?: boolean; // Multi-valued index
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -22,8 +22,8 @@ const schema = new Schema("myapp", {
|
|
|
22
22
|
users: {
|
|
23
23
|
fields: { name: "string", email: "string", role: "string" },
|
|
24
24
|
indexes: {
|
|
25
|
-
email: {},
|
|
26
|
-
role: {},
|
|
25
|
+
email: {}, // Simple index (field name = index name)
|
|
26
|
+
role: {}, // Simple index
|
|
27
27
|
name_role: { fields: ["name", "role"] }, // Compound index
|
|
28
28
|
},
|
|
29
29
|
},
|
|
@@ -72,7 +72,11 @@ const users = db.table("users");
|
|
|
72
72
|
const admins = await users.getAll("admin", "role");
|
|
73
73
|
|
|
74
74
|
// Range query on an indexed field
|
|
75
|
-
const recent = await users.between(
|
|
75
|
+
const recent = await users.between(
|
|
76
|
+
"createdAt",
|
|
77
|
+
new Date("2024-01-01"),
|
|
78
|
+
new Date(),
|
|
79
|
+
);
|
|
76
80
|
```
|
|
77
81
|
|
|
78
82
|
For more details, see [Index Management](../2.operations/5.indexes.md).
|
|
@@ -17,10 +17,10 @@ Field types describe the structure of each field:
|
|
|
17
17
|
|
|
18
18
|
```typescript
|
|
19
19
|
type FieldType =
|
|
20
|
-
| string
|
|
21
|
-
| Array<StringFieldType>
|
|
20
|
+
| string // Primitive type name (e.g., "string", "number")
|
|
21
|
+
| Array<StringFieldType> // Array of a given type
|
|
22
22
|
| { [subfield: string]: StringFieldType } // Nested object
|
|
23
|
-
| import("io-ts").Mixed;
|
|
23
|
+
| import("io-ts").Mixed; // io-ts codec at the field root
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
A field can be described either with the string-token vocabulary (primitives, arrays, nested records) or with a single [io-ts](https://github.com/gcanti/io-ts) codec at the root of the field. Codecs are not mixed inside string-tree records — use io-ts combinators (`t.type`, `t.array`, …) to nest within a codec.
|
|
@@ -14,11 +14,11 @@ interface Changes<T> {
|
|
|
14
14
|
|
|
15
15
|
## Properties
|
|
16
16
|
|
|
17
|
-
| Property | Type
|
|
18
|
-
| ------------ |
|
|
19
|
-
| `changeType` | `"added" \| "removed" \| "modified"`
|
|
20
|
-
| `oldValue` | `T \| undefined`
|
|
21
|
-
| `newValue` | `T \| undefined`
|
|
17
|
+
| Property | Type | Description |
|
|
18
|
+
| ------------ | ------------------------------------ | ------------------------------------------------------ |
|
|
19
|
+
| `changeType` | `"added" \| "removed" \| "modified"` | The kind of change that occurred. |
|
|
20
|
+
| `oldValue` | `T \| undefined` | The document before the change. Undefined for inserts. |
|
|
21
|
+
| `newValue` | `T \| undefined` | The document after the change. Undefined for deletes. |
|
|
22
22
|
|
|
23
23
|
## Determine the Change Type
|
|
24
24
|
|
package/docs/index.md
CHANGED
|
@@ -25,6 +25,7 @@ The Database interface provides AQL (Antelope Query Language), a database-agnost
|
|
|
25
25
|
- [Index Management](./2.operations/5.indexes.md) - Define and use secondary indexes
|
|
26
26
|
- [Filtering and Querying](./2.operations/6.filtering.md) - Filter, compare, and transform data
|
|
27
27
|
- [Lookup](./2.operations/7.lookup.md) - Foreign key joins and data population
|
|
28
|
+
- [Atomic Mutations](./2.operations/8.atomic_mutations.md) - Conditional single-record mutations and outcomes
|
|
28
29
|
|
|
29
30
|
- [**3. Results**](./3.results/1.index.md) - Result types and handling:
|
|
30
31
|
- [Insert Results](./3.results/2.write_results.md) - Results from insert operations
|
package/package.json
CHANGED
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/interface-database",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "AntelopeJS interface for database-agnostic query operations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"database",
|
|
8
|
+
"interface"
|
|
9
9
|
],
|
|
10
|
+
"license": "Apache-2.0",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
13
|
"url": "git+https://github.com/AntelopeJS/interface-database.git"
|
|
13
14
|
},
|
|
14
|
-
"license": "Apache-2.0",
|
|
15
|
-
"packageManager": "pnpm@10.6.5",
|
|
16
|
-
"main": "dist/index.js",
|
|
17
|
-
"types": "dist/index.d.ts",
|
|
18
15
|
"files": [
|
|
19
16
|
"dist",
|
|
20
17
|
"docs",
|
|
21
18
|
"skills"
|
|
22
19
|
],
|
|
20
|
+
"main": "dist/index.js",
|
|
21
|
+
"types": "dist/index.d.ts",
|
|
22
|
+
"typesVersions": {
|
|
23
|
+
"*": {
|
|
24
|
+
"atomic": [
|
|
25
|
+
"dist/atomic.d.ts"
|
|
26
|
+
],
|
|
27
|
+
"common": [
|
|
28
|
+
"dist/common.d.ts"
|
|
29
|
+
],
|
|
30
|
+
"datum": [
|
|
31
|
+
"dist/datum.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"query": [
|
|
34
|
+
"dist/query.d.ts"
|
|
35
|
+
],
|
|
36
|
+
"schema": [
|
|
37
|
+
"dist/schema.d.ts"
|
|
38
|
+
],
|
|
39
|
+
"selection": [
|
|
40
|
+
"dist/selection.d.ts"
|
|
41
|
+
],
|
|
42
|
+
"stream": [
|
|
43
|
+
"dist/stream.d.ts"
|
|
44
|
+
],
|
|
45
|
+
"valueproxy": [
|
|
46
|
+
"dist/valueproxy.d.ts"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
23
50
|
"exports": {
|
|
24
51
|
".": {
|
|
25
52
|
"types": "./dist/index.d.ts",
|
|
26
53
|
"default": "./dist/index.js"
|
|
27
54
|
},
|
|
55
|
+
"./atomic": {
|
|
56
|
+
"types": "./dist/atomic.d.ts",
|
|
57
|
+
"default": "./dist/atomic.js"
|
|
58
|
+
},
|
|
28
59
|
"./common": {
|
|
29
60
|
"types": "./dist/common.d.ts",
|
|
30
61
|
"default": "./dist/common.js"
|
|
@@ -55,26 +86,37 @@
|
|
|
55
86
|
},
|
|
56
87
|
"./package.json": "./package.json"
|
|
57
88
|
},
|
|
89
|
+
"publishConfig": {
|
|
90
|
+
"access": "public",
|
|
91
|
+
"provenance": true
|
|
92
|
+
},
|
|
58
93
|
"scripts": {
|
|
59
94
|
"build": "rimraf dist && tsc",
|
|
60
|
-
"format": "
|
|
61
|
-
"lint:fix": "
|
|
62
|
-
"lint": "
|
|
95
|
+
"format": "oxfmt .",
|
|
96
|
+
"lint:fix": "oxlint --fix",
|
|
97
|
+
"lint": "oxlint",
|
|
63
98
|
"prepack": "pnpm run build",
|
|
64
99
|
"release": "pnpm run lint && pnpm run prepack && release-it",
|
|
65
|
-
"test": "pnpm run build && ajs module test ."
|
|
100
|
+
"test": "pnpm run build && ajs module test .",
|
|
101
|
+
"format:check": "oxfmt --check .",
|
|
102
|
+
"knip": "knip"
|
|
66
103
|
},
|
|
67
104
|
"devDependencies": {
|
|
68
105
|
"@antelopejs/interface-core": ">=0.0.3 <1.0.0",
|
|
69
|
-
"@
|
|
106
|
+
"@antelopejs/tooling-configs": "^0.0.4",
|
|
70
107
|
"@types/chai": "^4.3.20",
|
|
71
108
|
"@types/mocha": "^10.0.10",
|
|
72
109
|
"@types/node": "^22.19.15",
|
|
73
110
|
"@types/semver": "^7.7.1",
|
|
74
111
|
"chai": "^4.5.0",
|
|
112
|
+
"eslint-plugin-perfectionist": "^5.11.0",
|
|
75
113
|
"fp-ts": "^2.16.0",
|
|
76
114
|
"io-ts": "^2.2.0",
|
|
115
|
+
"knip": "^6.34.0",
|
|
77
116
|
"mongodb-memory-server-core": "^11.0.1",
|
|
117
|
+
"oxfmt": "^0.66.0",
|
|
118
|
+
"oxlint": "1.81.0",
|
|
119
|
+
"oxlint-tsgolint": "^7.0.2001",
|
|
78
120
|
"release-it": "^19.2.4",
|
|
79
121
|
"release-it-changelogen": "^0.1.0",
|
|
80
122
|
"rimraf": "^6.1.3",
|
|
@@ -83,34 +125,7 @@
|
|
|
83
125
|
"peerDependencies": {
|
|
84
126
|
"@antelopejs/interface-core": ">=0.0.3 <1.0.0"
|
|
85
127
|
},
|
|
86
|
-
"
|
|
87
|
-
"access": "public"
|
|
88
|
-
},
|
|
89
|
-
"typesVersions": {
|
|
90
|
-
"*": {
|
|
91
|
-
"common": [
|
|
92
|
-
"dist/common.d.ts"
|
|
93
|
-
],
|
|
94
|
-
"datum": [
|
|
95
|
-
"dist/datum.d.ts"
|
|
96
|
-
],
|
|
97
|
-
"query": [
|
|
98
|
-
"dist/query.d.ts"
|
|
99
|
-
],
|
|
100
|
-
"schema": [
|
|
101
|
-
"dist/schema.d.ts"
|
|
102
|
-
],
|
|
103
|
-
"selection": [
|
|
104
|
-
"dist/selection.d.ts"
|
|
105
|
-
],
|
|
106
|
-
"stream": [
|
|
107
|
-
"dist/stream.d.ts"
|
|
108
|
-
],
|
|
109
|
-
"valueproxy": [
|
|
110
|
-
"dist/valueproxy.d.ts"
|
|
111
|
-
]
|
|
112
|
-
}
|
|
113
|
-
},
|
|
128
|
+
"packageManager": "pnpm@10.6.5",
|
|
114
129
|
"antelopeJs": {
|
|
115
130
|
"test": "src/antelope.test.ts",
|
|
116
131
|
"skills": [
|