@tldraw/store 4.1.0-canary.bdf9b3703a3d → 4.1.0-canary.bf8b596d2b31
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-cjs/index.d.ts +1884 -153
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/AtomMap.js +241 -1
- package/dist-cjs/lib/AtomMap.js.map +2 -2
- package/dist-cjs/lib/BaseRecord.js.map +2 -2
- package/dist-cjs/lib/ImmutableMap.js +141 -0
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
- package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +116 -21
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +233 -39
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreQueries.js +135 -22
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +207 -2
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/StoreSideEffects.js +102 -10
- package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-cjs/lib/migrate.js.map +2 -2
- package/dist-cjs/lib/setUtils.js.map +2 -2
- package/dist-esm/index.d.mts +1884 -153
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/AtomMap.mjs +241 -1
- package/dist-esm/lib/AtomMap.mjs.map +2 -2
- package/dist-esm/lib/BaseRecord.mjs.map +2 -2
- package/dist-esm/lib/ImmutableMap.mjs +141 -0
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
- package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +116 -21
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +233 -39
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreQueries.mjs +135 -22
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +207 -2
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/StoreSideEffects.mjs +102 -10
- package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/dist-esm/lib/migrate.mjs.map +2 -2
- package/dist-esm/lib/setUtils.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/AtomMap.ts +241 -1
- package/src/lib/BaseRecord.test.ts +44 -0
- package/src/lib/BaseRecord.ts +118 -4
- package/src/lib/ImmutableMap.test.ts +103 -0
- package/src/lib/ImmutableMap.ts +212 -0
- package/src/lib/IncrementalSetConstructor.test.ts +111 -0
- package/src/lib/IncrementalSetConstructor.ts +63 -6
- package/src/lib/RecordType.ts +149 -25
- package/src/lib/RecordsDiff.test.ts +144 -0
- package/src/lib/RecordsDiff.ts +145 -10
- package/src/lib/Store.test.ts +827 -0
- package/src/lib/Store.ts +533 -67
- package/src/lib/StoreQueries.test.ts +627 -0
- package/src/lib/StoreQueries.ts +194 -27
- package/src/lib/StoreSchema.test.ts +226 -0
- package/src/lib/StoreSchema.ts +386 -8
- package/src/lib/StoreSideEffects.test.ts +239 -19
- package/src/lib/StoreSideEffects.ts +266 -19
- package/src/lib/devFreeze.test.ts +137 -0
- package/src/lib/executeQuery.test.ts +481 -0
- package/src/lib/executeQuery.ts +80 -2
- package/src/lib/migrate.test.ts +400 -0
- package/src/lib/migrate.ts +187 -14
- package/src/lib/setUtils.test.ts +105 -0
- package/src/lib/setUtils.ts +44 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { devFreeze } from './devFreeze'
|
|
3
|
+
|
|
4
|
+
// Mock process.env for testing
|
|
5
|
+
const originalEnv = process.env.NODE_ENV
|
|
6
|
+
|
|
7
|
+
describe('devFreeze', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Reset any mocks
|
|
10
|
+
vi.restoreAllMocks()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('production mode behavior', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// Mock production environment
|
|
16
|
+
vi.stubGlobal('process', { env: { NODE_ENV: 'production' } })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should return objects unchanged in production mode', () => {
|
|
20
|
+
const obj = { a: 1, b: { c: 2 } }
|
|
21
|
+
const result = devFreeze(obj)
|
|
22
|
+
|
|
23
|
+
expect(result).toBe(obj) // Same reference
|
|
24
|
+
expect(Object.isFrozen(result)).toBe(false)
|
|
25
|
+
expect(Object.isFrozen(result.b)).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should not validate prototypes in production mode', () => {
|
|
29
|
+
const _consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
30
|
+
|
|
31
|
+
// Create object with custom prototype that would normally throw
|
|
32
|
+
class CustomClass {
|
|
33
|
+
value = 42
|
|
34
|
+
}
|
|
35
|
+
const obj = new CustomClass()
|
|
36
|
+
|
|
37
|
+
expect(() => devFreeze(obj)).not.toThrow()
|
|
38
|
+
expect(_consoleSpy).not.toHaveBeenCalled()
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe('development mode behavior', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
// Mock development environment
|
|
45
|
+
vi.stubGlobal('process', { env: { NODE_ENV: 'development' } })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should freeze objects recursively', () => {
|
|
49
|
+
const obj = {
|
|
50
|
+
a: 1,
|
|
51
|
+
b: {
|
|
52
|
+
c: 2,
|
|
53
|
+
d: {
|
|
54
|
+
e: 3,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
f: [1, { g: 4 }],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const result = devFreeze(obj)
|
|
61
|
+
|
|
62
|
+
expect(result).toBe(obj) // Same reference
|
|
63
|
+
expect(Object.isFrozen(result)).toBe(true)
|
|
64
|
+
expect(Object.isFrozen(result.b)).toBe(true)
|
|
65
|
+
expect(Object.isFrozen(result.b.d)).toBe(true)
|
|
66
|
+
expect(Object.isFrozen(result.f)).toBe(true)
|
|
67
|
+
expect(Object.isFrozen(result.f[1])).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should reject primitives', () => {
|
|
71
|
+
const _consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
72
|
+
|
|
73
|
+
// Primitives have built-in prototypes that aren't allowed
|
|
74
|
+
expect(() => devFreeze('string')).toThrow('cannot include non-js data in a record')
|
|
75
|
+
expect(() => devFreeze(42)).toThrow('cannot include non-js data in a record')
|
|
76
|
+
expect(() => devFreeze(true)).toThrow('cannot include non-js data in a record')
|
|
77
|
+
|
|
78
|
+
// null and undefined cause TypeError when Object.getPrototypeOf is called
|
|
79
|
+
expect(() => devFreeze(null)).toThrow('Cannot convert undefined or null to object')
|
|
80
|
+
expect(() => devFreeze(undefined)).toThrow('Cannot convert undefined or null to object')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should allow valid prototypes', () => {
|
|
84
|
+
// Object.prototype
|
|
85
|
+
const obj = { a: 1 }
|
|
86
|
+
expect(() => devFreeze(obj)).not.toThrow()
|
|
87
|
+
expect(Object.isFrozen(obj)).toBe(true)
|
|
88
|
+
|
|
89
|
+
// null prototype
|
|
90
|
+
const nullProtoObj = Object.create(null)
|
|
91
|
+
nullProtoObj.a = 1
|
|
92
|
+
expect(() => devFreeze(nullProtoObj)).not.toThrow()
|
|
93
|
+
expect(Object.isFrozen(nullProtoObj)).toBe(true)
|
|
94
|
+
|
|
95
|
+
// Arrays
|
|
96
|
+
const arr = [1, 2, 3]
|
|
97
|
+
expect(() => devFreeze(arr)).not.toThrow()
|
|
98
|
+
expect(Object.isFrozen(arr)).toBe(true)
|
|
99
|
+
|
|
100
|
+
// structuredClone objects
|
|
101
|
+
const cloned = structuredClone({ a: 1 })
|
|
102
|
+
expect(() => devFreeze(cloned)).not.toThrow()
|
|
103
|
+
expect(Object.isFrozen(cloned)).toBe(true)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should reject invalid prototypes', () => {
|
|
107
|
+
const _consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
108
|
+
|
|
109
|
+
// Custom class instances
|
|
110
|
+
class CustomClass {
|
|
111
|
+
value = 42
|
|
112
|
+
}
|
|
113
|
+
expect(() => devFreeze(new CustomClass())).toThrow('cannot include non-js data in a record')
|
|
114
|
+
|
|
115
|
+
// Built-in object types
|
|
116
|
+
expect(() => devFreeze(new Date())).toThrow('cannot include non-js data in a record')
|
|
117
|
+
expect(() => devFreeze(/regex/)).toThrow('cannot include non-js data in a record')
|
|
118
|
+
expect(() => devFreeze(new Map())).toThrow('cannot include non-js data in a record')
|
|
119
|
+
expect(() => devFreeze(new Set())).toThrow('cannot include non-js data in a record')
|
|
120
|
+
|
|
121
|
+
// Nested invalid objects
|
|
122
|
+
const objWithInvalidNested = {
|
|
123
|
+
valid: { a: 1 },
|
|
124
|
+
invalid: new Date(),
|
|
125
|
+
}
|
|
126
|
+
expect(() => devFreeze(objWithInvalidNested)).toThrow(
|
|
127
|
+
'cannot include non-js data in a record'
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Clean up after all tests
|
|
133
|
+
afterAll(() => {
|
|
134
|
+
// Restore original environment
|
|
135
|
+
vi.stubGlobal('process', { env: { NODE_ENV: originalEnv } })
|
|
136
|
+
})
|
|
137
|
+
})
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { BaseRecord, RecordId } from './BaseRecord'
|
|
3
|
+
import { executeQuery, objectMatchesQuery } from './executeQuery'
|
|
4
|
+
import { createRecordType } from './RecordType'
|
|
5
|
+
import { Store } from './Store'
|
|
6
|
+
import { StoreSchema } from './StoreSchema'
|
|
7
|
+
|
|
8
|
+
// Test record types
|
|
9
|
+
interface Author extends BaseRecord<'author', RecordId<Author>> {
|
|
10
|
+
name: string
|
|
11
|
+
age: number
|
|
12
|
+
isActive: boolean
|
|
13
|
+
publishedBooks: number
|
|
14
|
+
country: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
18
|
+
title: string
|
|
19
|
+
authorId: RecordId<Author>
|
|
20
|
+
publishedYear: number
|
|
21
|
+
inStock: boolean
|
|
22
|
+
rating: number
|
|
23
|
+
category: string
|
|
24
|
+
price: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Review extends BaseRecord<'review', RecordId<Review>> {
|
|
28
|
+
bookId: RecordId<Book>
|
|
29
|
+
rating: number
|
|
30
|
+
reviewerName: string
|
|
31
|
+
content: string
|
|
32
|
+
isVerified: boolean
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const Author = createRecordType<Author>('author', {
|
|
36
|
+
validator: {
|
|
37
|
+
validate(value) {
|
|
38
|
+
const author = value as Author
|
|
39
|
+
if (author.typeName !== 'author') throw Error('Invalid typeName')
|
|
40
|
+
if (!author.id.startsWith('author:')) throw Error('Invalid id')
|
|
41
|
+
if (!Number.isFinite(author.age)) throw Error('Invalid age')
|
|
42
|
+
if (author.age < 0) throw Error('Negative age')
|
|
43
|
+
if (typeof author.isActive !== 'boolean') throw Error('Invalid isActive')
|
|
44
|
+
return author
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
scope: 'document',
|
|
48
|
+
}).withDefaultProperties(() => ({
|
|
49
|
+
age: 25,
|
|
50
|
+
isActive: true,
|
|
51
|
+
publishedBooks: 0,
|
|
52
|
+
country: 'USA',
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
const Book = createRecordType<Book>('book', {
|
|
56
|
+
validator: {
|
|
57
|
+
validate(value) {
|
|
58
|
+
const book = value as Book
|
|
59
|
+
if (!book.id.startsWith('book:')) throw Error('Invalid book id')
|
|
60
|
+
if (book.typeName !== 'book') throw Error('Invalid book typeName')
|
|
61
|
+
if (typeof book.title !== 'string') throw Error('Invalid title')
|
|
62
|
+
if (!book.authorId.startsWith('author')) throw Error('Invalid authorId')
|
|
63
|
+
if (!Number.isFinite(book.publishedYear)) throw Error('Invalid publishedYear')
|
|
64
|
+
if (typeof book.inStock !== 'boolean') throw Error('Invalid inStock')
|
|
65
|
+
return book
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
scope: 'document',
|
|
69
|
+
}).withDefaultProperties(() => ({
|
|
70
|
+
rating: 0,
|
|
71
|
+
category: 'fiction',
|
|
72
|
+
price: 10.99,
|
|
73
|
+
}))
|
|
74
|
+
|
|
75
|
+
const Review = createRecordType<Review>('review', {
|
|
76
|
+
validator: {
|
|
77
|
+
validate(value) {
|
|
78
|
+
const review = value as Review
|
|
79
|
+
if (!review.id.startsWith('review:')) throw Error('Invalid review id')
|
|
80
|
+
if (review.typeName !== 'review') throw Error('Invalid review typeName')
|
|
81
|
+
if (!review.bookId.startsWith('book:')) throw Error('Invalid bookId')
|
|
82
|
+
if (!Number.isFinite(review.rating)) throw Error('Invalid rating')
|
|
83
|
+
if (typeof review.isVerified !== 'boolean') throw Error('Invalid isVerified')
|
|
84
|
+
return review
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
scope: 'document',
|
|
88
|
+
}).withDefaultProperties(() => ({
|
|
89
|
+
isVerified: false,
|
|
90
|
+
content: '',
|
|
91
|
+
reviewerName: 'Anonymous',
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
// Test data
|
|
95
|
+
const authors = {
|
|
96
|
+
asimov: Author.create({
|
|
97
|
+
name: 'Isaac Asimov',
|
|
98
|
+
age: 72,
|
|
99
|
+
publishedBooks: 200,
|
|
100
|
+
country: 'USA',
|
|
101
|
+
}),
|
|
102
|
+
gibson: Author.create({
|
|
103
|
+
name: 'William Gibson',
|
|
104
|
+
age: 75,
|
|
105
|
+
publishedBooks: 15,
|
|
106
|
+
country: 'USA',
|
|
107
|
+
}),
|
|
108
|
+
herbert: Author.create({
|
|
109
|
+
name: 'Frank Herbert',
|
|
110
|
+
age: 65,
|
|
111
|
+
publishedBooks: 30,
|
|
112
|
+
country: 'USA',
|
|
113
|
+
}),
|
|
114
|
+
bradbury: Author.create({
|
|
115
|
+
name: 'Ray Bradbury',
|
|
116
|
+
age: 91,
|
|
117
|
+
publishedBooks: 100,
|
|
118
|
+
isActive: false,
|
|
119
|
+
country: 'USA',
|
|
120
|
+
}),
|
|
121
|
+
clarke: Author.create({
|
|
122
|
+
name: 'Arthur C. Clarke',
|
|
123
|
+
age: 90,
|
|
124
|
+
publishedBooks: 80,
|
|
125
|
+
country: 'UK',
|
|
126
|
+
}),
|
|
127
|
+
adams: Author.create({
|
|
128
|
+
name: 'Douglas Adams',
|
|
129
|
+
age: 49,
|
|
130
|
+
publishedBooks: 12,
|
|
131
|
+
isActive: false,
|
|
132
|
+
country: 'UK',
|
|
133
|
+
}),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const books = {
|
|
137
|
+
foundation: Book.create({
|
|
138
|
+
title: 'Foundation',
|
|
139
|
+
authorId: authors.asimov.id,
|
|
140
|
+
publishedYear: 1951,
|
|
141
|
+
inStock: true,
|
|
142
|
+
rating: 5,
|
|
143
|
+
category: 'sci-fi',
|
|
144
|
+
price: 12.99,
|
|
145
|
+
}),
|
|
146
|
+
neuromancer: Book.create({
|
|
147
|
+
title: 'Neuromancer',
|
|
148
|
+
authorId: authors.gibson.id,
|
|
149
|
+
publishedYear: 1984,
|
|
150
|
+
inStock: true,
|
|
151
|
+
rating: 5,
|
|
152
|
+
category: 'cyberpunk',
|
|
153
|
+
price: 14.99,
|
|
154
|
+
}),
|
|
155
|
+
dune: Book.create({
|
|
156
|
+
title: 'Dune',
|
|
157
|
+
authorId: authors.herbert.id,
|
|
158
|
+
publishedYear: 1965,
|
|
159
|
+
inStock: false,
|
|
160
|
+
rating: 5,
|
|
161
|
+
category: 'sci-fi',
|
|
162
|
+
price: 13.99,
|
|
163
|
+
}),
|
|
164
|
+
fahrenheit451: Book.create({
|
|
165
|
+
title: 'Fahrenheit 451',
|
|
166
|
+
authorId: authors.bradbury.id,
|
|
167
|
+
publishedYear: 1953,
|
|
168
|
+
inStock: true,
|
|
169
|
+
rating: 4,
|
|
170
|
+
category: 'dystopian',
|
|
171
|
+
price: 11.99,
|
|
172
|
+
}),
|
|
173
|
+
childhood: Book.create({
|
|
174
|
+
title: "Childhood's End",
|
|
175
|
+
authorId: authors.clarke.id,
|
|
176
|
+
publishedYear: 1953,
|
|
177
|
+
inStock: true,
|
|
178
|
+
rating: 4,
|
|
179
|
+
category: 'sci-fi',
|
|
180
|
+
price: 10.99,
|
|
181
|
+
}),
|
|
182
|
+
hitchhiker: Book.create({
|
|
183
|
+
title: "The Hitchhiker's Guide to the Galaxy",
|
|
184
|
+
authorId: authors.adams.id,
|
|
185
|
+
publishedYear: 1979,
|
|
186
|
+
inStock: false,
|
|
187
|
+
rating: 5,
|
|
188
|
+
category: 'comedy-sci-fi',
|
|
189
|
+
price: 9.99,
|
|
190
|
+
}),
|
|
191
|
+
robots: Book.create({
|
|
192
|
+
title: 'I, Robot',
|
|
193
|
+
authorId: authors.asimov.id,
|
|
194
|
+
publishedYear: 1950,
|
|
195
|
+
inStock: true,
|
|
196
|
+
rating: 4,
|
|
197
|
+
category: 'sci-fi',
|
|
198
|
+
price: 12.99,
|
|
199
|
+
}),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const reviews = {
|
|
203
|
+
foundationReview1: Review.create({
|
|
204
|
+
bookId: books.foundation.id,
|
|
205
|
+
rating: 5,
|
|
206
|
+
reviewerName: 'Sci-Fi Fan',
|
|
207
|
+
content: 'Amazing epic saga!',
|
|
208
|
+
isVerified: true,
|
|
209
|
+
}),
|
|
210
|
+
foundationReview2: Review.create({
|
|
211
|
+
bookId: books.foundation.id,
|
|
212
|
+
rating: 4,
|
|
213
|
+
reviewerName: 'Book Lover',
|
|
214
|
+
content: 'Great world building',
|
|
215
|
+
isVerified: false,
|
|
216
|
+
}),
|
|
217
|
+
duneReview: Review.create({
|
|
218
|
+
bookId: books.dune.id,
|
|
219
|
+
rating: 5,
|
|
220
|
+
reviewerName: 'Epic Reader',
|
|
221
|
+
content: 'A masterpiece of science fiction',
|
|
222
|
+
isVerified: true,
|
|
223
|
+
}),
|
|
224
|
+
neuromancerReview: Review.create({
|
|
225
|
+
bookId: books.neuromancer.id,
|
|
226
|
+
rating: 3,
|
|
227
|
+
reviewerName: 'Casual Reader',
|
|
228
|
+
content: 'Hard to follow at times',
|
|
229
|
+
isVerified: false,
|
|
230
|
+
}),
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let store: Store<Author | Book | Review, unknown>
|
|
234
|
+
|
|
235
|
+
beforeEach(() => {
|
|
236
|
+
const schema = StoreSchema.create({
|
|
237
|
+
author: Author,
|
|
238
|
+
book: Book,
|
|
239
|
+
review: Review,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
store = new Store({
|
|
243
|
+
schema,
|
|
244
|
+
props: {},
|
|
245
|
+
}) as unknown as Store<Author | Book | Review, unknown>
|
|
246
|
+
|
|
247
|
+
// Add all test data to store
|
|
248
|
+
store.put([...Object.values(authors), ...Object.values(books), ...Object.values(reviews)])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('objectMatchesQuery', () => {
|
|
252
|
+
describe('equality matching (eq)', () => {
|
|
253
|
+
it('should match when property equals the target value', () => {
|
|
254
|
+
const book = books.foundation
|
|
255
|
+
const query = { inStock: { eq: true } }
|
|
256
|
+
|
|
257
|
+
expect(objectMatchesQuery(query, book)).toBe(true)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should not match when property does not equal the target value', () => {
|
|
261
|
+
const book = books.dune // inStock: false
|
|
262
|
+
const query = { inStock: { eq: true } }
|
|
263
|
+
|
|
264
|
+
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
265
|
+
})
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
describe('inequality matching (neq)', () => {
|
|
269
|
+
it('should match when property does not equal the target value', () => {
|
|
270
|
+
const book = books.foundation // category: 'sci-fi'
|
|
271
|
+
const query = { category: { neq: 'romance' } }
|
|
272
|
+
|
|
273
|
+
expect(objectMatchesQuery(query, book)).toBe(true)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should not match when property equals the target value', () => {
|
|
277
|
+
const book = books.foundation // category: 'sci-fi'
|
|
278
|
+
const query = { category: { neq: 'sci-fi' } }
|
|
279
|
+
|
|
280
|
+
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
describe('greater than matching (gt)', () => {
|
|
285
|
+
it('should match when numeric property is greater than target', () => {
|
|
286
|
+
const book = books.neuromancer // publishedYear: 1984
|
|
287
|
+
const query = { publishedYear: { gt: 1980 } }
|
|
288
|
+
|
|
289
|
+
expect(objectMatchesQuery(query, book)).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should not match when numeric property equals target', () => {
|
|
293
|
+
const book = books.neuromancer // publishedYear: 1984
|
|
294
|
+
const query = { publishedYear: { gt: 1984 } }
|
|
295
|
+
|
|
296
|
+
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should not match when property is not a number', () => {
|
|
300
|
+
const book = { title: '1984', publishedYear: '1984' as any }
|
|
301
|
+
const query = { publishedYear: { gt: 1980 } }
|
|
302
|
+
|
|
303
|
+
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
describe('multiple criteria matching', () => {
|
|
308
|
+
it('should match when all criteria are satisfied', () => {
|
|
309
|
+
const book = books.foundation // inStock: true, publishedYear: 1951, category: 'sci-fi'
|
|
310
|
+
const query = {
|
|
311
|
+
inStock: { eq: true },
|
|
312
|
+
publishedYear: { gt: 1950 },
|
|
313
|
+
category: { neq: 'romance' },
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
expect(objectMatchesQuery(query, book)).toBe(true)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should not match when any criteria is not satisfied', () => {
|
|
320
|
+
const book = books.dune // inStock: false, publishedYear: 1965, category: 'sci-fi'
|
|
321
|
+
const query = {
|
|
322
|
+
inStock: { eq: true }, // This will fail
|
|
323
|
+
publishedYear: { gt: 1950 },
|
|
324
|
+
category: { neq: 'romance' },
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe('edge cases', () => {
|
|
332
|
+
it('should return true for empty query', () => {
|
|
333
|
+
const book = books.foundation
|
|
334
|
+
const query = {}
|
|
335
|
+
|
|
336
|
+
expect(objectMatchesQuery(query, book)).toBe(true)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should handle missing properties gracefully', () => {
|
|
340
|
+
const book = { title: 'Test' } as any
|
|
341
|
+
const query = { nonexistentProperty: { eq: 'value' } }
|
|
342
|
+
|
|
343
|
+
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
describe('executeQuery', () => {
|
|
349
|
+
describe('equality queries (eq)', () => {
|
|
350
|
+
it('should find records with matching string values', () => {
|
|
351
|
+
const query = { category: { eq: 'sci-fi' } }
|
|
352
|
+
const result = executeQuery(store.query, 'book', query)
|
|
353
|
+
|
|
354
|
+
const expectedIds = new Set([
|
|
355
|
+
books.foundation.id,
|
|
356
|
+
books.dune.id,
|
|
357
|
+
books.childhood.id,
|
|
358
|
+
books.robots.id,
|
|
359
|
+
])
|
|
360
|
+
|
|
361
|
+
expect(result).toEqual(expectedIds)
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('should find records with matching RecordId values', () => {
|
|
365
|
+
const query = { authorId: { eq: authors.asimov.id } }
|
|
366
|
+
const result = executeQuery(store.query, 'book', query)
|
|
367
|
+
|
|
368
|
+
const expectedIds = new Set([books.foundation.id, books.robots.id])
|
|
369
|
+
|
|
370
|
+
expect(result).toEqual(expectedIds)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('should return empty set when no records match', () => {
|
|
374
|
+
const query = { category: { eq: 'nonexistent-category' } }
|
|
375
|
+
const result = executeQuery(store.query, 'book', query)
|
|
376
|
+
|
|
377
|
+
expect(result).toEqual(new Set())
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
describe('inequality queries (neq)', () => {
|
|
382
|
+
it('should find records that do not match string values', () => {
|
|
383
|
+
const query = { category: { neq: 'sci-fi' } }
|
|
384
|
+
const result = executeQuery(store.query, 'book', query)
|
|
385
|
+
|
|
386
|
+
const expectedIds = new Set([
|
|
387
|
+
books.neuromancer.id,
|
|
388
|
+
books.fahrenheit451.id,
|
|
389
|
+
books.hitchhiker.id,
|
|
390
|
+
])
|
|
391
|
+
|
|
392
|
+
expect(result).toEqual(expectedIds)
|
|
393
|
+
})
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
describe('greater than queries (gt)', () => {
|
|
397
|
+
it('should find records with values greater than threshold', () => {
|
|
398
|
+
const query = { publishedYear: { gt: 1970 } }
|
|
399
|
+
const result = executeQuery(store.query, 'book', query)
|
|
400
|
+
|
|
401
|
+
const expectedIds = new Set([books.neuromancer.id, books.hitchhiker.id])
|
|
402
|
+
|
|
403
|
+
expect(result).toEqual(expectedIds)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('should return empty set when no values are greater', () => {
|
|
407
|
+
const query = { publishedYear: { gt: 2000 } }
|
|
408
|
+
const result = executeQuery(store.query, 'book', query)
|
|
409
|
+
|
|
410
|
+
expect(result).toEqual(new Set())
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
describe('combined queries', () => {
|
|
415
|
+
it('should handle mixed query types', () => {
|
|
416
|
+
const query = {
|
|
417
|
+
inStock: { eq: true },
|
|
418
|
+
publishedYear: { gt: 1950 },
|
|
419
|
+
category: { neq: 'romance' },
|
|
420
|
+
}
|
|
421
|
+
const result = executeQuery(store.query, 'book', query)
|
|
422
|
+
|
|
423
|
+
const expectedIds = new Set([
|
|
424
|
+
books.foundation.id,
|
|
425
|
+
books.neuromancer.id,
|
|
426
|
+
books.fahrenheit451.id,
|
|
427
|
+
books.childhood.id,
|
|
428
|
+
])
|
|
429
|
+
|
|
430
|
+
expect(result).toEqual(expectedIds)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
describe('edge cases', () => {
|
|
435
|
+
it('should handle empty query', () => {
|
|
436
|
+
const query = {}
|
|
437
|
+
const result = executeQuery(store.query, 'book', query)
|
|
438
|
+
|
|
439
|
+
// Empty query in executeQuery returns empty set (special handling is done in StoreQueries)
|
|
440
|
+
expect(result).toEqual(new Set())
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('should handle different record types separately', () => {
|
|
444
|
+
// Query for authors, should not return books
|
|
445
|
+
const query = { name: { eq: 'Isaac Asimov' } }
|
|
446
|
+
const result = executeQuery(store.query, 'author', query)
|
|
447
|
+
|
|
448
|
+
const expectedIds = new Set([authors.asimov.id])
|
|
449
|
+
expect(result).toEqual(expectedIds)
|
|
450
|
+
|
|
451
|
+
// Same query on books should return empty
|
|
452
|
+
const bookResult = executeQuery(store.query, 'book', query as any)
|
|
453
|
+
expect(bookResult).toEqual(new Set())
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
describe('store integration', () => {
|
|
458
|
+
it('should update results when store changes', () => {
|
|
459
|
+
const query = { category: { eq: 'mystery' } }
|
|
460
|
+
|
|
461
|
+
// Initially no mystery books
|
|
462
|
+
let result = executeQuery(store.query, 'book', query)
|
|
463
|
+
expect(result).toEqual(new Set())
|
|
464
|
+
|
|
465
|
+
// Add a mystery book
|
|
466
|
+
const mysteryBook = Book.create({
|
|
467
|
+
title: 'Murder Mystery',
|
|
468
|
+
authorId: authors.asimov.id,
|
|
469
|
+
publishedYear: 2000,
|
|
470
|
+
inStock: true,
|
|
471
|
+
category: 'mystery',
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
store.put([mysteryBook])
|
|
475
|
+
|
|
476
|
+
// Should now find the mystery book
|
|
477
|
+
result = executeQuery(store.query, 'book', query)
|
|
478
|
+
expect(result).toEqual(new Set([mysteryBook.id]))
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
})
|