@omegup/msync 0.1.54 → 0.1.56
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 +388 -0
- package/index.d.ts +5 -2
- package/index.esm.js +3 -1
- package/index.js +3 -1
- package/package.json +11 -2
package/README.md
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# @omegup/msync
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@omegup/msync)
|
|
4
|
+
|
|
5
|
+
`msync` keeps derived MongoDB data in sync with the documents it comes from.
|
|
6
|
+
|
|
7
|
+
You describe the derivation the way you would write an aggregation: project the fields you read, join, reshape, group, and write the result back. `msync` runs that description once over the existing data, then again on every change. Joins, sums, and counts update from the delta. A removed source undoes the write that depended on it. A changed source rewrites only the documents that depend on it.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @omegup/msync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Peer runtime: MongoDB with change streams (a replica set), and TypeScript. `msync` enables change-stream pre- and post-images on every collection it watches.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Machine, from, staging } from '@omegup/msync'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## The principle
|
|
22
|
+
|
|
23
|
+
A stream is a named, typed function from source documents to stored documents.
|
|
24
|
+
|
|
25
|
+
1. **Sources are ordinary collections.** Every document `msync` reads or writes is a `Model`: it has `_id`, `touchedAt`, and `deletedAt`. `touchedAt` is the cluster time of the last write. `deletedAt: null` means the document is alive; a timestamp means it was removed. Deletes are soft, so a stream can see that a document went away and undo what it had written.
|
|
26
|
+
|
|
27
|
+
2. **You declare the slice you read.** A view is a collection plus a projection. The projection is the only data the rest of the pipeline is allowed to see, and the types follow it.
|
|
28
|
+
|
|
29
|
+
3. **Stages are valid as a full pass and as a delta.** `$lookup`, `$set`, `$replaceWith`, `$match`, and `$unwind` each know how to run over a snapshot and how to run over a before/after pair. You write them once.
|
|
30
|
+
|
|
31
|
+
4. **The last stage writes.** `$simpleMerge` and `$merge` patch fields onto an existing document. `$group` and `$groupId` fold many documents into one and keep the fold incremental. `$insert` creates documents in another collection and soft-deletes them when the source disappears.
|
|
32
|
+
|
|
33
|
+
5. **The name is the identity of the job.** Progress lives in a `__last` document keyed by that name. If the pipeline you registered under the name changes, `msync` tears the previous output down (unsets the fields it wrote, or soft-deletes the documents it inserted) and rebuilds.
|
|
34
|
+
|
|
35
|
+
6. **Streams compose by reading each other's fields.** A field whose name starts with `_` is treated as derived. A later stream that projects `_accountName` waits until that field exists, so you can add every stream to one machine and they still line up.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
source collection
|
|
39
|
+
│ change stream (pre-image + post-image)
|
|
40
|
+
▼
|
|
41
|
+
view ── projection, optional match
|
|
42
|
+
│
|
|
43
|
+
stages ── lookup, set, replace, match, unwind
|
|
44
|
+
│
|
|
45
|
+
sink ── merge | group | insert
|
|
46
|
+
│
|
|
47
|
+
▼
|
|
48
|
+
same collection, or another one
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Two ways in:
|
|
52
|
+
|
|
53
|
+
| | `from` | `staging` |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Use it when | The current document is enough, and you patch it in place | You join, group, insert, or need the previous value |
|
|
56
|
+
| What it stores | The resume token in `__last` | A `{collection}_{name}_snapshot` of before/after, plus `__last` |
|
|
57
|
+
| Sink | `$simpleMerge`, `$simpleInsert` | `$merge`, `$group`, `$groupId`, `$insert` |
|
|
58
|
+
| Can be the right side of `$lookup` | | Yes, after `.get()` |
|
|
59
|
+
|
|
60
|
+
`single` builds the same snapshot pipeline without the watch loop, for a one-shot read.
|
|
61
|
+
|
|
62
|
+
## A document
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import type { Model } from '@omegup/msync'
|
|
66
|
+
|
|
67
|
+
// Model is { _id, touchedAt, deletedAt? } intersected with your fields.
|
|
68
|
+
type Account = {
|
|
69
|
+
_id: string
|
|
70
|
+
name: string
|
|
71
|
+
createdAt: Date
|
|
72
|
+
_cohort?: string | null // written by a stream, e.g. "2026-03"
|
|
73
|
+
} & Model
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Your application sets `touchedAt` on every write (including its own updates) and sets `deletedAt` when it removes a document. `msync` does the same on the documents it creates. Bump `touchedAt` with the cluster time; streams resume from it.
|
|
77
|
+
|
|
78
|
+
Derived fields use a leading `_`. That is how a downstream stream knows the field is produced by an upstream one and should be waited for. Pass `needs: { plainField: 1 }` to wait on a field that does not use the prefix, or `needs: { _alreadyThere: 0 }` to read a `_` field without waiting.
|
|
79
|
+
|
|
80
|
+
## The shape of every stream
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
staging(view, 'unique-stream-name')
|
|
84
|
+
.with($lookup({ /* join, needs the left-hand stream */ }))
|
|
85
|
+
.then($replaceWith(/* reshape the current document */))
|
|
86
|
+
.then($match(/* keep or drop */))
|
|
87
|
+
.get() // freeze the pipeline; also what $lookup expects as `from`
|
|
88
|
+
.out($merge(/* ... */)(/* write */))
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `.with(fn)` wraps the stream so far. `$lookup` and `$outerLookup` are written this way, because a join has to see the left side.
|
|
92
|
+
- `.then(stage)` appends a stage that only sees the current document: `$set`, `$replaceWith`, `$match`, `$unwind`.
|
|
93
|
+
- `.get()` closes the pipeline.
|
|
94
|
+
- `.out(sink)` attaches the writer and returns a runner.
|
|
95
|
+
|
|
96
|
+
`from` has the same chain, and its stages are linear (`$set`, `$replaceWith`, `$match`). It has no snapshot, so it has no `$lookup`.
|
|
97
|
+
|
|
98
|
+
A projection entry is a pair, `[fieldName, 1]`, under the same name:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
projection: {
|
|
102
|
+
createdAt: ['createdAt', 1],
|
|
103
|
+
regionId: ['regionId', 1],
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A field expression is a pair too, `[fieldName, expr]`, inside `field({ ... })` and inside merge and group maps.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
root<Account>().of('createdAt').expr() // this document's field
|
|
111
|
+
val(' ') // a literal
|
|
112
|
+
concat(first, val(' '), last) // an expression
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`root<T>()` is the current document, typed as `T`. `.of('key')` steps into a field. `.expr()` turns the path into a value you can pass to `concat`, `$sum`, `$merge`, and the rest.
|
|
116
|
+
|
|
117
|
+
## Tutorial
|
|
118
|
+
|
|
119
|
+
One small domain runs through every example: accounts place orders, and plans are sold per region. Each function takes the collections it touches and returns a runner. The runners are what you hand to a `Machine`.
|
|
120
|
+
|
|
121
|
+
### 1. Derive a field from the document itself
|
|
122
|
+
|
|
123
|
+
An account has `createdAt`. List pages want the cohort (`YYYY-MM`) without parsing dates on every read. Keep `_cohort` on the account, and refresh it when `createdAt` changes.
|
|
124
|
+
|
|
125
|
+
`from` is enough: there is no join and no previous value to subtract.
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import type { Collection, Model, N, O } from '@omegup/msync'
|
|
129
|
+
import { $simpleMerge, from, monthPart, root } from '@omegup/msync'
|
|
130
|
+
|
|
131
|
+
type Account = O<{
|
|
132
|
+
_id: string
|
|
133
|
+
name: string
|
|
134
|
+
createdAt: Date
|
|
135
|
+
_cohort: string | N
|
|
136
|
+
}>
|
|
137
|
+
|
|
138
|
+
export const makeAccountCohortStream = (accounts: Collection<Account & Model>) =>
|
|
139
|
+
from(
|
|
140
|
+
{
|
|
141
|
+
collection: accounts,
|
|
142
|
+
projection: { createdAt: ['createdAt', 1] },
|
|
143
|
+
},
|
|
144
|
+
'account-cohort',
|
|
145
|
+
)
|
|
146
|
+
.get()
|
|
147
|
+
.out(
|
|
148
|
+
$simpleMerge<O<{ _cohort: string }>>()(accounts, {
|
|
149
|
+
_cohort: ['_cohort', monthPart(root<Account>().of('createdAt').expr())],
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`$simpleMerge` writes `_cohort` onto the same `_id`. The document is already there, so a missing target fails the merge. The next change of `createdAt` rewrites that account only.
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
{ _id: "a1", createdAt: 2026-03-02 } → { _cohort: "2026-03" }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. Copy a field across a reference
|
|
161
|
+
|
|
162
|
+
An order stores `accountId`. The account stores `name`. Reading an order should not look the account up again, and renaming an account should fix every order that points at it.
|
|
163
|
+
|
|
164
|
+
This needs `staging`. `$lookup` watches both sides: the order's `accountId`, and the account's `name`.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import type { Collection, Model, O, OPick, StrKey } from '@omegup/msync'
|
|
168
|
+
import { $lookup, $merge, root, staging } from '@omegup/msync'
|
|
169
|
+
|
|
170
|
+
type Account = O<{ _id: string; name: string }>
|
|
171
|
+
type Order = O<{ _id: string; accountId: string; total: number; _accountName?: string }>
|
|
172
|
+
|
|
173
|
+
type OrderView = OPick<Order & Model, '_id' | 'accountId'>
|
|
174
|
+
type AccountView = OPick<Account & Model, '_id' | 'name'>
|
|
175
|
+
type Joined = OrderView & { readonly account: AccountView }
|
|
176
|
+
|
|
177
|
+
export const makeOrderAccountNameStream = (
|
|
178
|
+
orders: Collection<Order & Model>,
|
|
179
|
+
accounts: Collection<Account & Model>,
|
|
180
|
+
) =>
|
|
181
|
+
staging<Order & Model, StrKey<OrderView>>(
|
|
182
|
+
{
|
|
183
|
+
collection: orders,
|
|
184
|
+
projection: { accountId: ['accountId', 1] },
|
|
185
|
+
},
|
|
186
|
+
'order-account-name',
|
|
187
|
+
)
|
|
188
|
+
.with(
|
|
189
|
+
$lookup({
|
|
190
|
+
as: 'account',
|
|
191
|
+
to: 'one',
|
|
192
|
+
from: staging(
|
|
193
|
+
{
|
|
194
|
+
collection: accounts,
|
|
195
|
+
projection: { name: ['name', 1] },
|
|
196
|
+
},
|
|
197
|
+
'accounts-for-orders',
|
|
198
|
+
).get(),
|
|
199
|
+
localField: root<OrderView>().of('accountId'),
|
|
200
|
+
}),
|
|
201
|
+
)
|
|
202
|
+
.get()
|
|
203
|
+
.out(
|
|
204
|
+
$merge<O<{ _accountName: string }>>()(orders, {
|
|
205
|
+
_accountName: ['_accountName', root<Joined>().of('account').of('name').expr()],
|
|
206
|
+
}),
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The inner `staging(...).get()` is a stream of its own, with its own snapshot. `$merge` patches `_accountName` back onto the order. A later stream that projects `_accountName` waits until this one has written it.
|
|
211
|
+
|
|
212
|
+
`$lookup` keeps documents that match. `$outerLookup` keeps the left document when the right side is missing, and types the joined field as `T | null`.
|
|
213
|
+
|
|
214
|
+
A lookup says where it points with `to`. `to: 'one'` means `localField` points at the right document's `_id`, so there is no `foreignField`. Each left row matches one right row, and the result keeps the left `_id`. `to: 'many'` is several matches per left row, or a join on some other field: pass `middle` and both fields. The lookup writes each row's `_id` as the left id, that string, and the right id. You do not build that id again afterwards.
|
|
215
|
+
|
|
216
|
+
### 3. Keep a total on the parent
|
|
217
|
+
|
|
218
|
+
Each order has an `accountId` and a `total`. The account should store `_lifetimeSpend`, the sum of its orders, including orders that change or disappear.
|
|
219
|
+
|
|
220
|
+
`$groupId` folds into a document that already exists. The group key is that document's `_id`, here the account id. `$sum` is delta-aware: a new order adds its total, an edited order subtracts the old total and adds the new one, a deleted order subtracts. The account is never recomputed from scratch.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import type { Collection, Model, O } from '@omegup/msync'
|
|
224
|
+
import { $groupId, $sum, notNull, root, staging } from '@omegup/msync'
|
|
225
|
+
|
|
226
|
+
type Account = O<{ _id: string; _lifetimeSpend?: number }>
|
|
227
|
+
type Order = O<{ _id: string; accountId: string; total: number }>
|
|
228
|
+
|
|
229
|
+
export const makeAccountSpendStream = (
|
|
230
|
+
orders: Collection<Order & Model>,
|
|
231
|
+
accounts: Collection<Account & Model>,
|
|
232
|
+
) =>
|
|
233
|
+
staging<Order & Model, '_id' | 'accountId' | 'total'>(
|
|
234
|
+
{
|
|
235
|
+
collection: orders,
|
|
236
|
+
projection: {
|
|
237
|
+
accountId: ['accountId', 1],
|
|
238
|
+
total: ['total', 1],
|
|
239
|
+
},
|
|
240
|
+
match: notNull(root<Order>().of('accountId').expr()),
|
|
241
|
+
},
|
|
242
|
+
'account-lifetime-spend',
|
|
243
|
+
)
|
|
244
|
+
.get()
|
|
245
|
+
.out(
|
|
246
|
+
$groupId<Order, O<{ _lifetimeSpend: number }>, {}, Account & Model>(
|
|
247
|
+
root<Order>().of('accountId').expr(),
|
|
248
|
+
{
|
|
249
|
+
_lifetimeSpend: ['_lifetimeSpend', $sum(root<Order>().of('total').expr())],
|
|
250
|
+
},
|
|
251
|
+
accounts,
|
|
252
|
+
{},
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Read `$groupId` as four arguments:
|
|
258
|
+
|
|
259
|
+
1. The group key, a string expression. It must equal `_id` on the target.
|
|
260
|
+
2. Accumulators. Each value is `['field', accumulator]`. These fields are maintained incrementally on the target.
|
|
261
|
+
3. The target collection.
|
|
262
|
+
4. Extra fields, recomputed from the accumulator output on every update. `{}` when the accumulator value is already what you want to store.
|
|
263
|
+
|
|
264
|
+
`$group` is the same fold when the target document does not exist yet. It stores the key in `_grp` and inserts. Use it for a summary collection, such as revenue per month. Use `$groupId` to hang a summary on a document you already have, such as spend on an account.
|
|
265
|
+
|
|
266
|
+
When the thing you are counting is a set of strings rather than a number, use `$countDict`. It stores a map of string to count, drops keys whose count falls to zero, and pairs with `$keys` in the extra-fields argument to publish the surviving strings as an array. A product that references tag ids, joined to the tag's name and folded with `$countDict`, keeps `_tags` on the product this way. The [guide](docs/guide.md) shows that accumulator next to `$sum`.
|
|
267
|
+
|
|
268
|
+
### 4. Materialize a collection of pairs
|
|
269
|
+
|
|
270
|
+
Plans are sold per region. Every account in a region is entitled to every plan in that region. Each pair is its own document in `entitlements`, with `_id` of `accountId-planId`, so other streams can aggregate entitlements directly. When the plan's price changes, the entitlement updates. When the account or the plan goes away, the entitlement is soft-deleted.
|
|
271
|
+
|
|
272
|
+
Several plans can share a region, so this lookup is `to: 'many'`. `middle: '-'` is required, and the lookup sets each row's `_id` to the account id, that hyphen, and the plan id. Copy `accountId` off `_id` before the join, because the composite id replaces it. `$replaceWith` keeps the id the lookup already assigned. `$insert` upserts by that `_id`, and on a deleted input it sets `deletedAt`.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import type { Collection, ID, Model, O, OPick } from '@omegup/msync'
|
|
276
|
+
import { $insert, $lookup, $replaceWith, $set, field, root, staging, to } from '@omegup/msync'
|
|
277
|
+
|
|
278
|
+
type Account = O<{ _id: string; regionId: string }>
|
|
279
|
+
type Plan = O<{ _id: string; regionId: string; price: number }>
|
|
280
|
+
type Entitlement = O<{
|
|
281
|
+
_id: string
|
|
282
|
+
accountId: string
|
|
283
|
+
planId: string
|
|
284
|
+
price: number
|
|
285
|
+
}>
|
|
286
|
+
|
|
287
|
+
export const makeEntitlementStream = (
|
|
288
|
+
accounts: Collection<Account & Model>,
|
|
289
|
+
plans: Collection<Plan & Model>,
|
|
290
|
+
entitlements: Collection<Entitlement & Model>,
|
|
291
|
+
) => {
|
|
292
|
+
type AccountView = OPick<Account & Model, keyof ID | 'regionId'>
|
|
293
|
+
type PlanView = OPick<Plan & Model, keyof ID | 'regionId' | 'price'>
|
|
294
|
+
type WithAccountId = AccountView & O<{ accountId: string }>
|
|
295
|
+
type Joined = WithAccountId & { readonly plan: PlanView }
|
|
296
|
+
|
|
297
|
+
return staging<Account & Model, keyof AccountView>(
|
|
298
|
+
{
|
|
299
|
+
collection: accounts,
|
|
300
|
+
projection: { regionId: ['regionId', 1] },
|
|
301
|
+
},
|
|
302
|
+
'entitlements-from-accounts',
|
|
303
|
+
)
|
|
304
|
+
.then(
|
|
305
|
+
$set<O<{ accountId: string }>>()({
|
|
306
|
+
accountId: ['accountId', to(root<AccountView>().of('_id').expr())],
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
.with(
|
|
310
|
+
$lookup({
|
|
311
|
+
as: 'plan',
|
|
312
|
+
to: 'many',
|
|
313
|
+
middle: '-',
|
|
314
|
+
from: staging(
|
|
315
|
+
{
|
|
316
|
+
collection: plans,
|
|
317
|
+
projection: {
|
|
318
|
+
regionId: ['regionId', 1],
|
|
319
|
+
price: ['price', 1],
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
'plans-for-entitlements',
|
|
323
|
+
).get(),
|
|
324
|
+
localField: root<WithAccountId>().of('regionId'),
|
|
325
|
+
foreignField: root<PlanView>().of('regionId'),
|
|
326
|
+
}),
|
|
327
|
+
)
|
|
328
|
+
.then(
|
|
329
|
+
$replaceWith(
|
|
330
|
+
field<Entitlement, Joined>({
|
|
331
|
+
_id: ['_id', root<Joined>().of('_id').expr()],
|
|
332
|
+
accountId: ['accountId', root<Joined>().of('accountId').expr()],
|
|
333
|
+
planId: ['planId', root<Joined>().of('plan').of('_id').expr()],
|
|
334
|
+
price: ['price', root<Joined>().of('plan').of('price').expr()],
|
|
335
|
+
}),
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
.get()
|
|
339
|
+
.out($insert(entitlements))
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The same account and the same plan always produce the same `_id`, because `middle` is part of the lookup. That is how the next run finds the row to update or retire.
|
|
344
|
+
|
|
345
|
+
## Run them together
|
|
346
|
+
|
|
347
|
+
A `Machine` merges runners and loops. Each tick reports which stream did the work.
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
import { Machine, prepare } from '@omegup/msync'
|
|
351
|
+
|
|
352
|
+
const client = await prepare() // connects with MONGO_URL, or use your own MongoClient
|
|
353
|
+
const db = client.db(process.env.MONGO_NAME)
|
|
354
|
+
|
|
355
|
+
const accounts = db.collection('accounts')
|
|
356
|
+
const orders = db.collection('orders')
|
|
357
|
+
const plans = db.collection('plans')
|
|
358
|
+
const entitlements = db.collection('entitlements')
|
|
359
|
+
|
|
360
|
+
const machine = new Machine()
|
|
361
|
+
machine.add(makeAccountCohortStream(accounts))
|
|
362
|
+
machine.add(makeOrderAccountNameStream(orders, accounts))
|
|
363
|
+
machine.add(makeAccountSpendStream(orders, accounts))
|
|
364
|
+
machine.add(makeEntitlementStream(accounts, plans, entitlements))
|
|
365
|
+
|
|
366
|
+
await machine.start(info => {
|
|
367
|
+
console.log(new Date(), info.debug)
|
|
368
|
+
})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
`.start` resolves only when it stops. Return `true` from the callback to stop after the current tick. Add streams in an order you can read — producers of `_` fields before the streams that project them — then let the machine run them side by side. The `_` prefix (and `needs`) is what makes a consumer skip a document until the producer has filled it.
|
|
372
|
+
|
|
373
|
+
Stream names are unique inside the process. Reusing a name from two different call sites throws. Pick a name that says what the stream maintains: `'account-cohort'`, `'order-account-name'`.
|
|
374
|
+
|
|
375
|
+
Split a large sync by area. Each area builds a `Machine` and returns it. The process adds every area and starts once:
|
|
376
|
+
|
|
377
|
+
```ts
|
|
378
|
+
const machine = new Machine()
|
|
379
|
+
machine.add(billing(client).runner())
|
|
380
|
+
machine.add(catalog(client).runner())
|
|
381
|
+
await machine.start(info => {
|
|
382
|
+
console.log(new Date(), info.debug)
|
|
383
|
+
})
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Next
|
|
387
|
+
|
|
388
|
+
[docs/guide.md](docs/guide.md) is the rest of the language: expressions, predicates, accumulators, the write modes side by side, and what the process stores while it runs.
|
package/index.d.ts
CHANGED
|
@@ -544,7 +544,7 @@ type Params<As extends string, LQ extends O, RQ extends O, RE extends RQ, S exte
|
|
|
544
544
|
as: AsLiteral<As>;
|
|
545
545
|
} & ({
|
|
546
546
|
to: 'one';
|
|
547
|
-
localField: Field<LQ, string>;
|
|
547
|
+
localField: Field<LQ, string | N>;
|
|
548
548
|
foreignField?: never;
|
|
549
549
|
middle?: never;
|
|
550
550
|
} | {
|
|
@@ -771,7 +771,10 @@ declare const sub: <T, D, Ctx, P extends O>(a: Expr<T, D, Ctx>, f: Field<P, D, C
|
|
|
771
771
|
declare const eqTyped: <T1 extends Dom, T2 extends Dom, F extends HKT<Dom>, C = unknown, Dom = unknown>(a: Expr<T1 | T2, App<F, T1 | T2>, C>, b: Expr<T1, App<F, T1 | T2>, C>) => BoolExpr<App<F, T1>, App<F, T2>, C>;
|
|
772
772
|
declare const ne: <T, D, C>(a: Expr<T, D, C>) => <K>(b: Expr<K, D, C>) => Expr<boolean, D, C>;
|
|
773
773
|
declare const notNull: <T, D, C>(a: Expr<T, D, C>) => Expr<boolean, D, C>;
|
|
774
|
-
declare const $ifNull:
|
|
774
|
+
declare const $ifNull: {
|
|
775
|
+
<R, D, C = unknown>(...expr: [...Expr<R | null | undefined, D, C>[], Expr<R, D, C>]): Expr<R, D, C>;
|
|
776
|
+
<R, D, C = unknown>(...expr: [Expr<R, D, C>, ...Expr<R | null | undefined, D, C>[]]): Expr<R, D, C>;
|
|
777
|
+
};
|
|
775
778
|
declare const exprMapVal: <K extends string, T extends Partial<Record<K, rawItem>>, D, C>(expr: Expr<K, D, C>, map: { readonly [P in K]: Expr<T[P], D, C>; }, or?: Expr<T[K], D, C>) => Expr<T[K & keyof T], D, C>;
|
|
776
779
|
declare const mapVal: <K extends string, T extends Partial<Record<K, rawItem>>, D, C>(expr: Expr<K, D, C>, map: T, or: T[K & keyof T]) => Expr<T[K & keyof T], D, C>;
|
|
777
780
|
declare const setField: <K extends string, T, V, D, C>({ field, input, value, }: {
|
package/index.esm.js
CHANGED
|
@@ -942,7 +942,9 @@ const getWhenMatched = (whenNotMatched) => {
|
|
|
942
942
|
return link().with($replaceWith_(ite(same, root().of('old').expr(), root().of('merged').expr()))).stages;
|
|
943
943
|
};
|
|
944
944
|
const $mergeX = (out, keys, f, map, ext, update) => {
|
|
945
|
-
const patch = mapExact(keys, v =>
|
|
945
|
+
const patch = mapExact(keys, (v) => {
|
|
946
|
+
return $ifNull(sub(v, f), nil);
|
|
947
|
+
});
|
|
946
948
|
const filter = mapExactToObject(ext, v => ({ $eq: v }));
|
|
947
949
|
const setDeleted = out.whenNotMatched === 'discard';
|
|
948
950
|
const replacer = map(field(omitPick().backward(spread(patch, {
|
package/index.js
CHANGED
|
@@ -944,7 +944,9 @@ const getWhenMatched = (whenNotMatched) => {
|
|
|
944
944
|
return link().with($replaceWith_(ite(same, root().of('old').expr(), root().of('merged').expr()))).stages;
|
|
945
945
|
};
|
|
946
946
|
const $mergeX = (out, keys, f, map, ext, update) => {
|
|
947
|
-
const patch = mapExact(keys, v =>
|
|
947
|
+
const patch = mapExact(keys, (v) => {
|
|
948
|
+
return $ifNull(sub(v, f), nil);
|
|
949
|
+
});
|
|
948
950
|
const filter = mapExactToObject(ext, v => ({ $eq: v }));
|
|
949
951
|
const setDeleted = out.whenNotMatched === 'discard';
|
|
950
952
|
const replacer = map(field(omitPick().backward(spread(patch, {
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
+
"name": "@omegup/msync",
|
|
3
|
+
"version": "0.1.56",
|
|
4
|
+
"description": "Keeps derived MongoDB data in sync with the documents it comes from.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/Omegup/msync.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/Omegup/msync#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/Omegup/msync/issues"
|
|
12
|
+
},
|
|
2
13
|
"main": "index.js",
|
|
3
14
|
"module": "index.esm.js",
|
|
4
15
|
"typings": "index.d.ts",
|
|
5
|
-
"name": "@omegup/msync",
|
|
6
|
-
"version": "0.1.54",
|
|
7
16
|
"dependencies": {
|
|
8
17
|
"dayjs": "^1.11.9",
|
|
9
18
|
"dotenv": "^16.3.1",
|