@owlmeans/state 0.1.18-rc.8 → 0.1.18
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 +173 -68
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/state/SKILL.md +137 -58
- package/build/consts.local.d.ts +10 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +10 -0
- package/build/consts.local.js.map +1 -0
- package/build/errors.d.ts +12 -5
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +16 -13
- package/build/errors.js.map +1 -1
- package/build/helper.d.ts +16 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +14 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +3 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/resource.d.ts +12 -5
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +317 -207
- package/build/resource.js.map +1 -1
- package/build/types.d.ts +91 -50
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +15 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/criteria.d.ts.map +1 -1
- package/build/utils/criteria.js.map +1 -1
- package/build/utils/model.d.ts +10 -2
- package/build/utils/model.d.ts.map +1 -1
- package/build/utils/model.js +26 -22
- package/build/utils/model.js.map +1 -1
- package/build/utils/types.d.ts +15 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/package.json +4 -4
- package/src/consts.local.ts +11 -0
- package/src/errors.ts +16 -13
- package/src/helper.ts +17 -0
- package/src/index.ts +3 -2
- package/src/resource.ts +359 -241
- package/src/types.local.ts +16 -0
- package/src/types.ts +98 -55
- package/src/utils/model.ts +35 -28
- package/src/utils/types.ts +15 -0
- package/tests/resource.spec.ts +347 -90
- package/build/.gitkeep +0 -0
- package/src/consts.ts +0 -3
- package/src/utils/criteria.ts +0 -252
- package/src/utils/index.ts +0 -3
- package/tests/criteria.spec.ts +0 -121
package/src/utils/criteria.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { UnsupportedArgumentError } from '@owlmeans/resource'
|
|
2
|
-
import type { ListCriteria, ListSort, ResourceRecord } from '@owlmeans/resource'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* In-memory evaluation of `ListCriteria`.
|
|
6
|
-
*
|
|
7
|
-
* The operator set mirrors `@owlmeans/postgres-resource` on purpose: one criteria shape has to
|
|
8
|
-
* mean the same thing whether a list is filtered in SQL by an endpoint or locally by a screen
|
|
9
|
-
* reading the state resource. A client that learns `{ status: ['open', 'done'] }` means "either
|
|
10
|
-
* of these" must not meet a different answer when the same object reaches the server.
|
|
11
|
-
*
|
|
12
|
-
* Two deliberate differences, both because a state resource carries no schema:
|
|
13
|
-
*
|
|
14
|
-
* - An unknown key cannot be detected. Postgres throws on one (a typo silently widening a query
|
|
15
|
-
* to the whole table is worth being loud about); here the property is simply absent from the
|
|
16
|
-
* record, so the record does not match. There is nothing to compare it against.
|
|
17
|
-
* - There is no jsonb/column distinction, so a dotted key always reaches into the record.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const COMPARISON = new Set(['$eq', '$ne', '$gt', '$gte', '$lt', '$lte'])
|
|
21
|
-
|
|
22
|
-
/** Dates compare by instant; everything else compares as it is. */
|
|
23
|
-
const comparable = (value: unknown): unknown =>
|
|
24
|
-
value instanceof Date ? value.getTime() : value
|
|
25
|
-
|
|
26
|
-
/** Reach `profile.city` inside a record. A missing step yields `undefined`, never a throw. */
|
|
27
|
-
const reach = (record: unknown, path: string[]): unknown =>
|
|
28
|
-
path.reduce<unknown>(
|
|
29
|
-
(value, step) => value == null || typeof value !== 'object'
|
|
30
|
-
? undefined
|
|
31
|
-
: (value as Record<string, unknown>)[step],
|
|
32
|
-
record
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* An object naming at least one `$` key is a spec, not a value to compare against.
|
|
37
|
-
* A Date and an array are values even though both are objects.
|
|
38
|
-
*/
|
|
39
|
-
const isOperatorSpec = (value: unknown): value is Record<string, unknown> =>
|
|
40
|
-
value != null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
|
|
41
|
-
&& Object.keys(value).some(key => key.startsWith('$'))
|
|
42
|
-
|
|
43
|
-
const equal = (left: unknown, right: unknown): boolean => {
|
|
44
|
-
if (left instanceof Date || right instanceof Date) {
|
|
45
|
-
return comparable(left) === comparable(right)
|
|
46
|
-
}
|
|
47
|
-
if (Array.isArray(left) && Array.isArray(right)) {
|
|
48
|
-
return left.length === right.length && left.every((entry, index) => equal(entry, right[index]))
|
|
49
|
-
}
|
|
50
|
-
return left === right
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** `$in` / `$nin`. `null` in the list matches an absent property, matching the SQL widening. */
|
|
54
|
-
const inList = (value: unknown, operand: unknown): boolean => {
|
|
55
|
-
const values = Array.isArray(operand) ? operand : [operand]
|
|
56
|
-
return values.some(entry => entry == null ? value == null : equal(value, entry))
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const text = (value: unknown): string => value == null ? '' : `${value}`
|
|
60
|
-
|
|
61
|
-
/** `%` and `_` are the SQL wildcards; `\` escapes them. */
|
|
62
|
-
const likeToRegExp = (pattern: string, flags: string): RegExp => {
|
|
63
|
-
let source = ''
|
|
64
|
-
for (let index = 0; index < pattern.length; index++) {
|
|
65
|
-
const char = pattern[index]!
|
|
66
|
-
if (char === '\\' && index + 1 < pattern.length) {
|
|
67
|
-
source += pattern[++index]!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
68
|
-
continue
|
|
69
|
-
}
|
|
70
|
-
if (char === '%') { source += '.*'; continue }
|
|
71
|
-
if (char === '_') { source += '.'; continue }
|
|
72
|
-
source += char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return new RegExp(`^${source}$`, flags)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @throws {UnsupportedArgumentError} on an operator this store cannot answer.
|
|
80
|
-
*/
|
|
81
|
-
const applyOperators = (value: unknown, spec: Record<string, unknown>): boolean => {
|
|
82
|
-
for (const [operator, operand] of Object.entries(spec)) {
|
|
83
|
-
if (COMPARISON.has(operator)) {
|
|
84
|
-
const left = comparable(value)
|
|
85
|
-
const right = comparable(operand)
|
|
86
|
-
const matched =
|
|
87
|
-
operator === '$eq' ? equal(value, operand)
|
|
88
|
-
: operator === '$ne' ? !equal(value, operand)
|
|
89
|
-
: left == null || right == null ? false
|
|
90
|
-
: operator === '$gt' ? (left as never) > (right as never)
|
|
91
|
-
: operator === '$gte' ? (left as never) >= (right as never)
|
|
92
|
-
: operator === '$lt' ? (left as never) < (right as never)
|
|
93
|
-
: (left as never) <= (right as never)
|
|
94
|
-
if (!matched) return false
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
97
|
-
switch (operator) {
|
|
98
|
-
case '$in':
|
|
99
|
-
if (!inList(value, operand)) return false
|
|
100
|
-
break
|
|
101
|
-
case '$nin':
|
|
102
|
-
if (inList(value, operand)) return false
|
|
103
|
-
break
|
|
104
|
-
case '$exists':
|
|
105
|
-
if ((value != null) !== (operand !== false)) return false
|
|
106
|
-
break
|
|
107
|
-
case '$null':
|
|
108
|
-
if ((value == null) !== (operand !== false)) return false
|
|
109
|
-
break
|
|
110
|
-
case '$like':
|
|
111
|
-
if (!likeToRegExp(text(operand), '').test(text(value))) return false
|
|
112
|
-
break
|
|
113
|
-
case '$ilike':
|
|
114
|
-
if (!likeToRegExp(text(operand), 'i').test(text(value))) return false
|
|
115
|
-
break
|
|
116
|
-
case '$regex':
|
|
117
|
-
if (!new RegExp(text(operand)).test(text(value))) return false
|
|
118
|
-
break
|
|
119
|
-
case '$startsWith':
|
|
120
|
-
if (!text(value).startsWith(text(operand))) return false
|
|
121
|
-
break
|
|
122
|
-
case '$endsWith':
|
|
123
|
-
if (!text(value).endsWith(text(operand))) return false
|
|
124
|
-
break
|
|
125
|
-
case '$between': {
|
|
126
|
-
if (!Array.isArray(operand) || operand.length !== 2) {
|
|
127
|
-
throw new UnsupportedArgumentError(`criteria:$between`)
|
|
128
|
-
}
|
|
129
|
-
const left = comparable(value)
|
|
130
|
-
if (left == null) return false
|
|
131
|
-
const from = comparable(operand[0])
|
|
132
|
-
const to = comparable(operand[1])
|
|
133
|
-
if ((left as never) < (from as never) || (left as never) > (to as never)) return false
|
|
134
|
-
break
|
|
135
|
-
}
|
|
136
|
-
case '$contains': {
|
|
137
|
-
const operands = Array.isArray(operand) ? operand : [operand]
|
|
138
|
-
const values = Array.isArray(value) ? value : [value]
|
|
139
|
-
if (!operands.every(entry => values.some(candidate => equal(candidate, entry)))) return false
|
|
140
|
-
break
|
|
141
|
-
}
|
|
142
|
-
case '$contained': {
|
|
143
|
-
const operands = Array.isArray(operand) ? operand : [operand]
|
|
144
|
-
const values = Array.isArray(value) ? value : [value]
|
|
145
|
-
if (!values.every(candidate => operands.some(entry => equal(candidate, entry)))) return false
|
|
146
|
-
break
|
|
147
|
-
}
|
|
148
|
-
case '$overlaps': {
|
|
149
|
-
const operands = Array.isArray(operand) ? operand : [operand]
|
|
150
|
-
const values = Array.isArray(value) ? value : [value]
|
|
151
|
-
if (!values.some(candidate => operands.some(entry => equal(candidate, entry)))) return false
|
|
152
|
-
break
|
|
153
|
-
}
|
|
154
|
-
default:
|
|
155
|
-
throw new UnsupportedArgumentError(`criteria-operator:${operator}`)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return true
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Whether one record satisfies the criteria.
|
|
164
|
-
*
|
|
165
|
-
* `undefined` as a criteria value is skipped rather than compared — it is what an optional filter
|
|
166
|
-
* looks like when nothing was chosen, and treating it as "must be undefined" makes an unset
|
|
167
|
-
* dropdown filter the list down to nothing.
|
|
168
|
-
*
|
|
169
|
-
* @throws {UnsupportedArgumentError} on an unknown operator.
|
|
170
|
-
*/
|
|
171
|
-
export const matchCriteria = <T extends ResourceRecord>(
|
|
172
|
-
record: T, criteria?: ListCriteria
|
|
173
|
-
): boolean => {
|
|
174
|
-
for (const [key, raw] of Object.entries(criteria ?? {})) {
|
|
175
|
-
if (raw === undefined) {
|
|
176
|
-
continue
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (key === '$and' || key === '$or') {
|
|
180
|
-
const parts = (Array.isArray(raw) ? raw : [raw]) as ListCriteria[]
|
|
181
|
-
if (parts.length < 1) continue
|
|
182
|
-
const matched = key === '$and'
|
|
183
|
-
? parts.every(part => matchCriteria(record, part))
|
|
184
|
-
: parts.some(part => matchCriteria(record, part))
|
|
185
|
-
if (!matched) return false
|
|
186
|
-
continue
|
|
187
|
-
}
|
|
188
|
-
if (key === '$not') {
|
|
189
|
-
if (matchCriteria(record, raw as ListCriteria)) return false
|
|
190
|
-
continue
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const value = reach(record, key.split('.'))
|
|
194
|
-
|
|
195
|
-
if (raw === null) {
|
|
196
|
-
if (value != null) return false
|
|
197
|
-
continue
|
|
198
|
-
}
|
|
199
|
-
if (isOperatorSpec(raw)) {
|
|
200
|
-
if (!applyOperators(value, raw as Record<string, unknown>)) return false
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
if (Array.isArray(raw)) {
|
|
204
|
-
/**
|
|
205
|
-
* A bare array means "any of these", exactly as it does against a relational store.
|
|
206
|
-
* Exact array equality stays reachable as `{ $eq: [...] }`.
|
|
207
|
-
*/
|
|
208
|
-
if (!inList(value, raw)) return false
|
|
209
|
-
continue
|
|
210
|
-
}
|
|
211
|
-
if (!equal(value, raw)) return false
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return true
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Every record the criteria accepts, in insertion order. */
|
|
218
|
-
export const filterRecords = <T extends ResourceRecord>(
|
|
219
|
-
records: T[], criteria?: ListCriteria
|
|
220
|
-
): T[] => criteria == null || Object.keys(criteria).length < 1
|
|
221
|
-
? [...records]
|
|
222
|
-
: records.filter(record => matchCriteria(record, criteria))
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Sort a copy by `ListPager.sort`. `[field, true]` is descending — the same mapping mongo's
|
|
226
|
-
* `order ? -1 : 1` and the postgres builder use, so a pager written for one store orders the
|
|
227
|
-
* same way against another.
|
|
228
|
-
*/
|
|
229
|
-
export const sortRecords = <T extends ResourceRecord>(records: T[], sort?: ListSort[]): T[] => {
|
|
230
|
-
if (sort == null || sort.length < 1) {
|
|
231
|
-
return [...records]
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const rules = sort.map(entry => Array.isArray(entry)
|
|
235
|
-
? { field: entry[0], desc: entry[1] === true }
|
|
236
|
-
: { field: entry, desc: false })
|
|
237
|
-
|
|
238
|
-
return [...records].sort((left, right) => {
|
|
239
|
-
for (const { field, desc } of rules) {
|
|
240
|
-
const a = comparable(reach(left, field.split('.')))
|
|
241
|
-
const b = comparable(reach(right, field.split('.')))
|
|
242
|
-
if (a == null && b == null) continue
|
|
243
|
-
/** An absent value sorts last ascending, which is where a reader expects "no value". */
|
|
244
|
-
if (a == null) return 1
|
|
245
|
-
if (b == null) return -1
|
|
246
|
-
if ((a as never) < (b as never)) return desc ? 1 : -1
|
|
247
|
-
if ((a as never) > (b as never)) return desc ? -1 : 1
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return 0
|
|
251
|
-
})
|
|
252
|
-
}
|
package/src/utils/index.ts
DELETED
package/tests/criteria.spec.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { filterRecords, matchCriteria, sortRecords } from '@owlmeans/state'
|
|
3
|
-
|
|
4
|
-
interface Task {
|
|
5
|
-
id?: string
|
|
6
|
-
title?: string
|
|
7
|
-
status?: string
|
|
8
|
-
points?: number
|
|
9
|
-
tags?: string[]
|
|
10
|
-
owner?: { name?: string, team?: string }
|
|
11
|
-
due?: Date
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const tasks: Task[] = [
|
|
15
|
-
{ id: 'a', title: 'Alpha', status: 'open', points: 3, tags: ['ui', 'urgent'], owner: { name: 'ann', team: 'core' } },
|
|
16
|
-
{ id: 'b', title: 'Beta', status: 'done', points: 8, tags: ['api'], owner: { name: 'bob', team: 'core' } },
|
|
17
|
-
{ id: 'c', title: 'Gamma', status: 'open', points: 1, tags: [], owner: { name: 'cid', team: 'ops' } },
|
|
18
|
-
{ id: 'd', title: 'Delta', points: 5 },
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
const ids = (records: Task[]): string[] => records.map(record => record.id!)
|
|
22
|
-
|
|
23
|
-
describe('@owlmeans/state — criteria', () => {
|
|
24
|
-
test('a bare value is field equality', () => {
|
|
25
|
-
expect(ids(filterRecords(tasks, { status: 'open' }))).toEqual(['a', 'c'])
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
// The same shape reaching a postgres resource means IN, so it has to mean IN here too —
|
|
29
|
-
// otherwise one filter object answers two different questions depending on who evaluates it.
|
|
30
|
-
test('a bare array is $in', () => {
|
|
31
|
-
expect(ids(filterRecords(tasks, { status: ['open', 'done'] }))).toEqual(['a', 'b', 'c'])
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('comparison operators', () => {
|
|
35
|
-
expect(ids(filterRecords(tasks, { points: { $gte: 5 } }))).toEqual(['b', 'd'])
|
|
36
|
-
expect(ids(filterRecords(tasks, { points: { $lt: 3 } }))).toEqual(['c'])
|
|
37
|
-
expect(ids(filterRecords(tasks, { points: { $gt: 1, $lte: 5 } }))).toEqual(['a', 'd'])
|
|
38
|
-
expect(ids(filterRecords(tasks, { status: { $ne: 'open' } }))).toEqual(['b', 'd'])
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('$in, $nin, $exists, $null', () => {
|
|
42
|
-
expect(ids(filterRecords(tasks, { id: { $in: ['a', 'd'] } }))).toEqual(['a', 'd'])
|
|
43
|
-
expect(ids(filterRecords(tasks, { id: { $nin: ['a', 'd'] } }))).toEqual(['b', 'c'])
|
|
44
|
-
expect(ids(filterRecords(tasks, { status: { $exists: false } }))).toEqual(['d'])
|
|
45
|
-
expect(ids(filterRecords(tasks, { status: { $null: true } }))).toEqual(['d'])
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test('text operators', () => {
|
|
49
|
-
expect(ids(filterRecords(tasks, { title: { $startsWith: 'A' } }))).toEqual(['a'])
|
|
50
|
-
expect(ids(filterRecords(tasks, { title: { $endsWith: 'a' } }))).toEqual(['a', 'b', 'c', 'd'])
|
|
51
|
-
expect(ids(filterRecords(tasks, { title: { $endsWith: 'ta' } }))).toEqual(['b', 'd'])
|
|
52
|
-
expect(ids(filterRecords(tasks, { title: { $like: '%amm%' } }))).toEqual(['c'])
|
|
53
|
-
expect(ids(filterRecords(tasks, { title: { $ilike: 'alpha' } }))).toEqual(['a'])
|
|
54
|
-
expect(ids(filterRecords(tasks, { title: { $regex: '^(Al|Be)' } }))).toEqual(['a', 'b'])
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('$between spans inclusively and rejects a malformed operand', () => {
|
|
58
|
-
expect(ids(filterRecords(tasks, { points: { $between: [3, 5] } }))).toEqual(['a', 'd'])
|
|
59
|
-
expect(() => filterRecords(tasks, { points: { $between: [3] } as never })).toThrow()
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('array operators', () => {
|
|
63
|
-
expect(ids(filterRecords(tasks, { tags: { $contains: 'urgent' } }))).toEqual(['a'])
|
|
64
|
-
expect(ids(filterRecords(tasks, { tags: { $overlaps: ['api', 'urgent'] } }))).toEqual(['a', 'b'])
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('logical operators nest', () => {
|
|
68
|
-
expect(ids(filterRecords(tasks, { $or: [{ status: 'done' }, { points: { $lt: 2 } }] }))).toEqual(['b', 'c'])
|
|
69
|
-
expect(ids(filterRecords(tasks, { $and: [{ status: 'open' }, { points: { $gt: 2 } }] }))).toEqual(['a'])
|
|
70
|
-
expect(ids(filterRecords(tasks, { $not: { status: 'open' } }))).toEqual(['b', 'd'])
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('a dotted key reaches into the record', () => {
|
|
74
|
-
expect(ids(filterRecords(tasks, { 'owner.team': 'core' }))).toEqual(['a', 'b'])
|
|
75
|
-
expect(ids(filterRecords(tasks, { 'owner.name': { $in: ['cid'] } }))).toEqual(['c'])
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// An optional filter with nothing chosen arrives as `undefined`. Comparing against it would
|
|
79
|
-
// filter the list down to records that lack the property — an empty screen for a dropdown the
|
|
80
|
-
// user never touched.
|
|
81
|
-
test('an undefined criteria value is skipped, null is a real IS NULL', () => {
|
|
82
|
-
expect(ids(filterRecords(tasks, { status: undefined }))).toEqual(['a', 'b', 'c', 'd'])
|
|
83
|
-
expect(ids(filterRecords(tasks, { status: null as never }))).toEqual(['d'])
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
// No schema means no way to tell a typo from a property this record simply lacks. Postgres
|
|
87
|
-
// throws; here the honest answer is that nothing matches.
|
|
88
|
-
test('an unknown key matches nothing rather than throwing', () => {
|
|
89
|
-
expect(ids(filterRecords(tasks, { nonesuch: 'x' }))).toEqual([])
|
|
90
|
-
expect(matchCriteria(tasks[0]!, { nonesuch: 'x' })).toBe(false)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
test('an unknown operator is rejected', () => {
|
|
94
|
-
expect(() => filterRecords(tasks, { points: { $nope: 1 } as never })).toThrow()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
test('empty criteria keeps everything', () => {
|
|
98
|
-
expect(ids(filterRecords(tasks, {}))).toEqual(['a', 'b', 'c', 'd'])
|
|
99
|
-
expect(ids(filterRecords(tasks, undefined))).toEqual(['a', 'b', 'c', 'd'])
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test('dates compare by instant', () => {
|
|
103
|
-
const dated: Task[] = [
|
|
104
|
-
{ id: 'x', due: new Date('2026-01-01') },
|
|
105
|
-
{ id: 'y', due: new Date('2026-06-01') },
|
|
106
|
-
]
|
|
107
|
-
expect(ids(filterRecords(dated, { due: { $gt: new Date('2026-03-01') } }))).toEqual(['y'])
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('sort ascending, descending and by a dotted key; absent values sort last', () => {
|
|
111
|
-
expect(ids(sortRecords(tasks, ['points']))).toEqual(['c', 'a', 'd', 'b'])
|
|
112
|
-
expect(ids(sortRecords(tasks, [['points', true]]))).toEqual(['b', 'd', 'a', 'c'])
|
|
113
|
-
expect(ids(sortRecords(tasks, ['owner.name']))).toEqual(['a', 'b', 'c', 'd'])
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test('sorting does not mutate the input', () => {
|
|
117
|
-
const input = [...tasks]
|
|
118
|
-
sortRecords(input, [['points', true]])
|
|
119
|
-
expect(ids(input)).toEqual(['a', 'b', 'c', 'd'])
|
|
120
|
-
})
|
|
121
|
-
})
|