Add Prettier formatting and Gitea Actions CI (format, typecheck, test, build)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qxbkp4tA7G5SCWjzWuxmE
This commit is contained in:
@@ -32,7 +32,8 @@ describe('encodeIndexValue', () => {
|
||||
fc.assert(
|
||||
fc.property(indexValue, separators, (v, sep) => {
|
||||
const decoded = decodeIndexValue(encodeIndexValue(v, sep), sep)
|
||||
if (typeof v === 'number' && v === 0) expect(decoded).toBe(0) // -0 normalised
|
||||
if (typeof v === 'number' && v === 0)
|
||||
expect(decoded).toBe(0) // -0 normalised
|
||||
else expect(decoded).toEqual(v)
|
||||
}),
|
||||
)
|
||||
|
||||
+1
-10
@@ -1,15 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import fc from 'fast-check'
|
||||
import {
|
||||
escapeSegment,
|
||||
joinPath,
|
||||
pathKeys,
|
||||
splitPath,
|
||||
succ,
|
||||
toSegment,
|
||||
unescapeSegment,
|
||||
validateSeparator,
|
||||
} from '../../src/core/path.js'
|
||||
import { escapeSegment, joinPath, pathKeys, splitPath, succ, toSegment, unescapeSegment, validateSeparator } from '../../src/core/path.js'
|
||||
import { InvalidKeyError } from '../../src/errors.js'
|
||||
|
||||
const separators = fc.constantFrom('/', ':', '.', '|', '#', '\\', '!')
|
||||
|
||||
@@ -157,7 +157,7 @@ describe.each([
|
||||
it('evaluates comparison, logical and element operators', async () => {
|
||||
const { users } = await seed()
|
||||
// Unsorted results come in index or key order depending on the plan; compare as sets.
|
||||
const q = async (f: Filter) => (await keysOf(users.find(f))).sort()
|
||||
const q = async (f: Filter) => (await keysOf(users.find(f))).sort()
|
||||
expect(await q({})).toEqual(['a', 'b', 'c', 'd', 'e'])
|
||||
expect(await q({ age: 20 })).toEqual(['b', 'd'])
|
||||
expect(await q({ age: { $gte: 20, $lt: 40 } })).toEqual(['a', 'b', 'd'])
|
||||
@@ -186,7 +186,9 @@ describe.each([
|
||||
expect(await keysOf(users.find({}, { sort: { name: -1 }, skip: 1, limit: 2 }))).toEqual(['d', 'c'])
|
||||
expect(await keysOf(users.find({ age: { $gte: 20 } }, { sort: { age: -1 }, limit: 2 }))).toEqual(['c', 'a'])
|
||||
expect(await keysOf(users.find({ 'address.city': 'London' }, { sort: { age: 1 } }))).toEqual(['e', 'a', 'c'])
|
||||
expect(await collect(users.find({ age: 36 }, { projection: { name: 1, 'address.city': 1 } }))).toEqual([{ key: 'a', data: { name: 'Ada', address: { city: 'London' } } }])
|
||||
expect(await collect(users.find({ age: 36 }, { projection: { name: 1, 'address.city': 1 } }))).toEqual([
|
||||
{ key: 'a', data: { name: 'Ada', address: { city: 'London' } } },
|
||||
])
|
||||
expect(await collect(users.find({ age: 36 }, { projection: { tags: 0, address: 0, email: 0 } }))).toEqual([{ key: 'a', data: { name: 'Ada', age: 36 } }])
|
||||
expect(await users.findOne({ email: 'bob@x' })).toEqual({ key: 'b', data: expect.objectContaining({ name: 'Bob' }) })
|
||||
expect(await users.findOne({ email: 'nobody' })).toBeUndefined()
|
||||
@@ -278,7 +280,11 @@ describe.each([
|
||||
|
||||
it('catchUp() repairs a subtree', async () => {
|
||||
const { users, docs, events } = await lagged()
|
||||
await events.child('users').child('9').child('posts').append('p', [{ op: 'set', doc: { createdAt: 'x' } }])
|
||||
await events
|
||||
.child('users')
|
||||
.child('9')
|
||||
.child('posts')
|
||||
.append('p', [{ op: 'set', doc: { createdAt: 'x' } }])
|
||||
expect(await docs.catchUp()).toBe(2)
|
||||
expect(await users.get('1')).toEqual({ n: 2, email: 'b@x' })
|
||||
expect(await users.child('9').child('posts').get('p')).toEqual({ createdAt: 'x' })
|
||||
@@ -339,28 +345,34 @@ describe('property: indexed find equals unindexed find', () => {
|
||||
|
||||
it('returns the same results in the same order', async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(fc.array(fc.tuple(fc.constantFrom('1', '2', '3', '4', '5'), doc), { maxLength: 12 }), filter, sort, fc.option(fc.integer({ min: 1, max: 4 }), { nil: undefined }), async (writes, f, s, limit) => {
|
||||
const plain = new EventSourcedDocumentStorage(new KVEventStorage(new KVStorage(new InMemoryKVDriver())), { warnOnFullScan: false }).child('users')
|
||||
const indexed = new EventSourcedDocumentStorage(new KVEventStorage(new KVStorage(new InMemoryKVDriver())), {
|
||||
warnOnFullScan: false,
|
||||
indexes: { users: userIndexes.users!.map((i) => ({ ...i, unique: false })) },
|
||||
}).child('users')
|
||||
for (const [k, v] of writes) {
|
||||
await plain.set(k, v)
|
||||
await indexed.set(k, v)
|
||||
}
|
||||
const opts = { ...(s ? { sort: s } : {}), ...(limit ? { limit } : {}) }
|
||||
const a = await collect(plain.find(f, opts))
|
||||
const b = await collect(indexed.find(f, opts))
|
||||
if (s) {
|
||||
expect(b).toEqual(a)
|
||||
return
|
||||
}
|
||||
// Unsorted order depends on the plan: compare as sets (a limited result is any subset of that size).
|
||||
const all = new Set((await collect(plain.find(f))).map((r) => r.key))
|
||||
expect(b).toHaveLength(a.length)
|
||||
for (const r of b) expect(all.has(r.key)).toBe(true)
|
||||
}),
|
||||
fc.asyncProperty(
|
||||
fc.array(fc.tuple(fc.constantFrom('1', '2', '3', '4', '5'), doc), { maxLength: 12 }),
|
||||
filter,
|
||||
sort,
|
||||
fc.option(fc.integer({ min: 1, max: 4 }), { nil: undefined }),
|
||||
async (writes, f, s, limit) => {
|
||||
const plain = new EventSourcedDocumentStorage(new KVEventStorage(new KVStorage(new InMemoryKVDriver())), { warnOnFullScan: false }).child('users')
|
||||
const indexed = new EventSourcedDocumentStorage(new KVEventStorage(new KVStorage(new InMemoryKVDriver())), {
|
||||
warnOnFullScan: false,
|
||||
indexes: { users: userIndexes.users!.map((i) => ({ ...i, unique: false })) },
|
||||
}).child('users')
|
||||
for (const [k, v] of writes) {
|
||||
await plain.set(k, v)
|
||||
await indexed.set(k, v)
|
||||
}
|
||||
const opts = { ...(s ? { sort: s } : {}), ...(limit ? { limit } : {}) }
|
||||
const a = await collect(plain.find(f, opts))
|
||||
const b = await collect(indexed.find(f, opts))
|
||||
if (s) {
|
||||
expect(b).toEqual(a)
|
||||
return
|
||||
}
|
||||
// Unsorted order depends on the plan: compare as sets (a limited result is any subset of that size).
|
||||
const all = new Set((await collect(plain.find(f))).map((r) => r.key))
|
||||
expect(b).toHaveLength(a.length)
|
||||
for (const r of b) expect(all.has(r.key)).toBe(true)
|
||||
},
|
||||
),
|
||||
{ numRuns: 300 },
|
||||
)
|
||||
})
|
||||
|
||||
@@ -45,7 +45,13 @@ describe('filter semantics', () => {
|
||||
|
||||
describe('update resolution', () => {
|
||||
it('$pull by sub-document filter and by condition', () => {
|
||||
const state = { items: [{ k: 1, v: 'a' }, { k: 2, v: 'b' }], n: [1, 5, 9] }
|
||||
const state = {
|
||||
items: [
|
||||
{ k: 1, v: 'a' },
|
||||
{ k: 2, v: 'b' },
|
||||
],
|
||||
n: [1, 5, 9],
|
||||
}
|
||||
const patch = resolveUpdate(state, { $pull: { items: { k: 2 }, n: { $gte: 5 } } })
|
||||
expect(applyPatch(state, patch)).toEqual({ items: [{ k: 1, v: 'a' }], n: [1] })
|
||||
})
|
||||
|
||||
+1
-2
@@ -40,8 +40,7 @@ export class CountingDriver implements KVDriver {
|
||||
readonly inner: KVDriver = new InMemoryKVDriver(),
|
||||
hide: Capability[] = [],
|
||||
) {
|
||||
if (inner.setIfAbsent && !hide.includes('setIfAbsent'))
|
||||
this.setIfAbsent = (e: [string, unknown][]) => (this.#count('setIfAbsent'), inner.setIfAbsent!(e))
|
||||
if (inner.setIfAbsent && !hide.includes('setIfAbsent')) this.setIfAbsent = (e: [string, unknown][]) => (this.#count('setIfAbsent'), inner.setIfAbsent!(e))
|
||||
if (inner.batch && !hide.includes('batch')) this.batch = (ops: BatchOp[]) => (this.#count('batch'), inner.batch!(ops))
|
||||
if (inner.deleteRange && !hide.includes('deleteRange'))
|
||||
this.deleteRange = (gte: string, lt: string) => (this.#count('deleteRange'), inner.deleteRange!(gte, lt))
|
||||
|
||||
+14
-3
@@ -204,7 +204,10 @@ describe('index declarations', () => {
|
||||
})
|
||||
|
||||
describe('property: findBy equals brute-force filter', () => {
|
||||
const val = fc.record({ age: fc.option(fc.integer({ min: -3, max: 3 }), { nil: undefined }), tags: fc.array(fc.constantFrom('a', 'b', 'c/d'), { maxLength: 3 }) })
|
||||
const val = fc.record({
|
||||
age: fc.option(fc.integer({ min: -3, max: 3 }), { nil: undefined }),
|
||||
tags: fc.array(fc.constantFrom('a', 'b', 'c/d'), { maxLength: 3 }),
|
||||
})
|
||||
const op = fc.oneof(
|
||||
fc.record({ t: fc.constant('set' as const), k: fc.constantFrom('1', '2', '3', 'x/y'), v: val }),
|
||||
fc.record({ t: fc.constant('del' as const), k: fc.constantFrom('1', '2', '3', 'x/y') }),
|
||||
@@ -226,11 +229,19 @@ describe('property: findBy equals brute-force filter', () => {
|
||||
}
|
||||
}
|
||||
const got = (await collect(users.findBy('byAge', { gte: lo, lt: hi }))).map(([k]) => k).sort()
|
||||
const exp = [...model].filter(([, v]) => v.age !== undefined && v.age >= lo && v.age < hi).map(([k]) => k).sort()
|
||||
const exp = [...model]
|
||||
.filter(([, v]) => v.age !== undefined && v.age >= lo && v.age < hi)
|
||||
.map(([k]) => k)
|
||||
.sort()
|
||||
expect(got).toEqual(exp)
|
||||
for (const tag of ['a', 'b', 'c/d']) {
|
||||
const g = (await collect(users.findBy('byTag', tag))).map(([k]) => k).sort()
|
||||
expect(g).toEqual([...model].filter(([, v]) => v.tags?.includes(tag)).map(([k]) => k).sort())
|
||||
expect(g).toEqual(
|
||||
[...model]
|
||||
.filter(([, v]) => v.tags?.includes(tag))
|
||||
.map(([k]) => k)
|
||||
.sort(),
|
||||
)
|
||||
}
|
||||
// No stale entries are ever left behind by clean writes.
|
||||
let entryCount = 0
|
||||
|
||||
+51
-47
@@ -293,56 +293,60 @@ describe('model-based (random ops vs reference Map)', () => {
|
||||
] as const) {
|
||||
it(`matches the model (${name})`, async () => {
|
||||
await fc.assert(
|
||||
fc.asyncProperty(fc.array(opArb, { maxLength: 30 }), fc.array(fc.array(segArb, { maxLength: 2 }), { minLength: 1, maxLength: 3 }), async (ops, probes) => {
|
||||
const db = new KVStorage(make())
|
||||
const model = new Map<string, unknown>()
|
||||
for (const op of ops) {
|
||||
switch (op.t) {
|
||||
case 'set': {
|
||||
if (op.selfForm) await nodeAt(db, op.path).set(op.value)
|
||||
else await nodeAt(db, op.path.slice(0, -1)).set(op.path.at(-1)!, op.value)
|
||||
model.set(enc(op.path), op.value)
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
if (op.selfForm) await nodeAt(db, op.path).delete()
|
||||
else await nodeAt(db, op.path.slice(0, -1)).delete(op.path.at(-1)!)
|
||||
model.delete(enc(op.path))
|
||||
break
|
||||
}
|
||||
case 'deleteTree': {
|
||||
await nodeAt(db, op.path).deleteTree()
|
||||
for (const k of [...model.keys()]) if (isPrefix(op.path, JSON.parse(k))) model.delete(k)
|
||||
break
|
||||
}
|
||||
case 'setMany': {
|
||||
await nodeAt(db, op.parent).setMany(op.entries)
|
||||
for (const [k, v] of op.entries) model.set(enc([...op.parent, k]), v)
|
||||
break
|
||||
fc.asyncProperty(
|
||||
fc.array(opArb, { maxLength: 30 }),
|
||||
fc.array(fc.array(segArb, { maxLength: 2 }), { minLength: 1, maxLength: 3 }),
|
||||
async (ops, probes) => {
|
||||
const db = new KVStorage(make())
|
||||
const model = new Map<string, unknown>()
|
||||
for (const op of ops) {
|
||||
switch (op.t) {
|
||||
case 'set': {
|
||||
if (op.selfForm) await nodeAt(db, op.path).set(op.value)
|
||||
else await nodeAt(db, op.path.slice(0, -1)).set(op.path.at(-1)!, op.value)
|
||||
model.set(enc(op.path), op.value)
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
if (op.selfForm) await nodeAt(db, op.path).delete()
|
||||
else await nodeAt(db, op.path.slice(0, -1)).delete(op.path.at(-1)!)
|
||||
model.delete(enc(op.path))
|
||||
break
|
||||
}
|
||||
case 'deleteTree': {
|
||||
await nodeAt(db, op.path).deleteTree()
|
||||
for (const k of [...model.keys()]) if (isPrefix(op.path, JSON.parse(k))) model.delete(k)
|
||||
break
|
||||
}
|
||||
case 'setMany': {
|
||||
await nodeAt(db, op.parent).setMany(op.entries)
|
||||
for (const [k, v] of op.entries) model.set(enc([...op.parent, k]), v)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [k, v] of model) expect(await nodeAt(db, JSON.parse(k)).get()).toEqual(v)
|
||||
for (const parent of probes) {
|
||||
const node = nodeAt(db, parent)
|
||||
const exp = expectedChildren(model, parent)
|
||||
const keys = exp.map((e) => e.key)
|
||||
expect(await collect(node.keys())).toEqual(keys)
|
||||
expect(await collect(node.keys({ reverse: true }))).toEqual([...keys].reverse())
|
||||
expect(await collect(node.entries())).toEqual(exp.filter((e) => e.hasOwn).map((e) => [e.key, e.own]))
|
||||
expect(await collect(node.entries({ reverse: true }))).toEqual(
|
||||
exp
|
||||
.filter((e) => e.hasOwn)
|
||||
.map((e) => [e.key, e.own])
|
||||
.reverse(),
|
||||
)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
expect(await collect(node.keys({ after: keys[i]! }))).toEqual(keys.slice(i + 1))
|
||||
expect(await collect(node.keys({ after: keys[i]!, reverse: true }))).toEqual(keys.slice(0, i).reverse())
|
||||
expect(await collect(node.keys({ limit: i + 1 }))).toEqual(keys.slice(0, i + 1))
|
||||
for (const [k, v] of model) expect(await nodeAt(db, JSON.parse(k)).get()).toEqual(v)
|
||||
for (const parent of probes) {
|
||||
const node = nodeAt(db, parent)
|
||||
const exp = expectedChildren(model, parent)
|
||||
const keys = exp.map((e) => e.key)
|
||||
expect(await collect(node.keys())).toEqual(keys)
|
||||
expect(await collect(node.keys({ reverse: true }))).toEqual([...keys].reverse())
|
||||
expect(await collect(node.entries())).toEqual(exp.filter((e) => e.hasOwn).map((e) => [e.key, e.own]))
|
||||
expect(await collect(node.entries({ reverse: true }))).toEqual(
|
||||
exp
|
||||
.filter((e) => e.hasOwn)
|
||||
.map((e) => [e.key, e.own])
|
||||
.reverse(),
|
||||
)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
expect(await collect(node.keys({ after: keys[i]! }))).toEqual(keys.slice(i + 1))
|
||||
expect(await collect(node.keys({ after: keys[i]!, reverse: true }))).toEqual(keys.slice(0, i).reverse())
|
||||
expect(await collect(node.keys({ limit: i + 1 }))).toEqual(keys.slice(0, i + 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
),
|
||||
{ numRuns: 300 },
|
||||
)
|
||||
}, 60_000)
|
||||
|
||||
+24
-19
@@ -36,26 +36,31 @@ describe('ChunkedSortedMap (model-based, tiny chunks)', () => {
|
||||
|
||||
it('iterators resume correctly after mutations between steps', () => {
|
||||
fc.assert(
|
||||
fc.property(fc.uniqueArray(key, { minLength: 1, maxLength: 60 }), fc.array(fc.tuple(fc.boolean(), key), { maxLength: 40 }), fc.boolean(), (initial, muts, reverse) => {
|
||||
const m = new ChunkedSortedMap<number>(3)
|
||||
initial.forEach((k) => m.set(k, 0))
|
||||
const live = new Set(initial)
|
||||
const seen: string[] = []
|
||||
let step = 0
|
||||
for (const k of m.keys({ reverse })) {
|
||||
seen.push(k)
|
||||
const mut = muts[step++]
|
||||
if (mut) {
|
||||
const [add, mk] = mut
|
||||
if (add) (m.set(mk, 1), live.add(mk))
|
||||
else (m.delete(mk), live.delete(mk))
|
||||
fc.property(
|
||||
fc.uniqueArray(key, { minLength: 1, maxLength: 60 }),
|
||||
fc.array(fc.tuple(fc.boolean(), key), { maxLength: 40 }),
|
||||
fc.boolean(),
|
||||
(initial, muts, reverse) => {
|
||||
const m = new ChunkedSortedMap<number>(3)
|
||||
initial.forEach((k) => m.set(k, 0))
|
||||
const live = new Set(initial)
|
||||
const seen: string[] = []
|
||||
let step = 0
|
||||
for (const k of m.keys({ reverse })) {
|
||||
seen.push(k)
|
||||
const mut = muts[step++]
|
||||
if (mut) {
|
||||
const [add, mk] = mut
|
||||
if (add) (m.set(mk, 1), live.add(mk))
|
||||
else (m.delete(mk), live.delete(mk))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Strictly monotone, and every key present throughout the iteration was visited.
|
||||
for (let i = 1; i < seen.length; i++) expect(reverse ? seen[i]! < seen[i - 1]! : seen[i]! > seen[i - 1]!).toBe(true)
|
||||
const stable = initial.filter((k) => !muts.some(([, mk]) => mk === k))
|
||||
for (const k of stable) expect(seen).toContain(k)
|
||||
}),
|
||||
// Strictly monotone, and every key present throughout the iteration was visited.
|
||||
for (let i = 1; i < seen.length; i++) expect(reverse ? seen[i]! < seen[i - 1]! : seen[i]! > seen[i - 1]!).toBe(true)
|
||||
const stable = initial.filter((k) => !muts.some(([, mk]) => mk === k))
|
||||
for (const k of stable) expect(seen).toContain(k)
|
||||
},
|
||||
),
|
||||
{ numRuns: 500 },
|
||||
)
|
||||
})
|
||||
|
||||
+16
-7
@@ -47,12 +47,18 @@ describe('KV types (section 7)', () => {
|
||||
db.child('users').child('1').set({ username: 'a', age: 1 })
|
||||
// @ts-expect-error wrong value type
|
||||
db.child('users').set('1', { username: 'a' })
|
||||
db.child('profile').child('9').setMany([
|
||||
['age', 3],
|
||||
['username', 'x'],
|
||||
])
|
||||
// @ts-expect-error value doesn't match its key
|
||||
db.child('profile').child('9').setMany([['age', 'x']])
|
||||
db.child('profile')
|
||||
.child('9')
|
||||
.setMany([
|
||||
['age', 3],
|
||||
['username', 'x'],
|
||||
])
|
||||
db.child('profile')
|
||||
.child('9')
|
||||
.setMany([
|
||||
// @ts-expect-error value doesn't match its key
|
||||
['age', 'x'],
|
||||
])
|
||||
})
|
||||
|
||||
it('types listings', () => {
|
||||
@@ -82,7 +88,10 @@ describe('event types', () => {
|
||||
orders.child('o1').append([{ type: 'paid' }])
|
||||
// @ts-expect-error not an OrderEvent
|
||||
orders.append('o1', [{ type: 'refunded' }])
|
||||
orders.child('o1').child('shipments').append('s1', [{ carrier: 'x' }])
|
||||
orders
|
||||
.child('o1')
|
||||
.child('shipments')
|
||||
.append('s1', [{ carrier: 'x' }])
|
||||
})
|
||||
|
||||
it('types read results', () => {
|
||||
|
||||
Reference in New Issue
Block a user