Appwrite-Abfragen: select, notEqual, countDocuments und Nachbau dazu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c1055e8a0
commit
2dd9c0dec7
3 files changed
+190
-7
No files matched your search
+144
-1
@@ -17,7 +17,7 @@ const plain = await startAppwriteMock()
|
||||
const st = await startFakeStripe()
|
||||
after(() => Promise.all([aw.close(), plain.close(), st.close()]))
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { Query, createDocument, createDocuments, getDocument, updateDocument, listDocuments, listAllDocuments } = await import(
|
||||
const { Query, createDocument, createDocuments, getDocument, updateDocument, listDocuments, listAllDocuments, countDocuments } = await import(
|
||||
'../server/services/appwriteAdmin.js'
|
||||
)
|
||||
|
||||
@@ -39,6 +39,9 @@ test('Query-Helfer bauen die Appwrite-Abfragen', () => {
|
||||
assert.deepEqual(Query.greaterThanEqual('startsAt', 'a'), { method: 'greaterThanEqual', attribute: 'startsAt', values: ['a'] })
|
||||
assert.deepEqual(Query.lessThanEqual('startsAt', 'b'), { method: 'lessThanEqual', attribute: 'startsAt', values: ['b'] })
|
||||
assert.deepEqual(Query.cursorAfter('doc1'), { method: 'cursorAfter', values: ['doc1'] })
|
||||
assert.deepEqual(Query.select(['a', 'b']), { method: 'select', values: ['a', 'b'] })
|
||||
assert.deepEqual(Query.notEqual('status', 'x'), { method: 'notEqual', attribute: 'status', values: ['x'] })
|
||||
assert.deepEqual(Query.notEqual('status', ['x', 'y']), { method: 'notEqual', attribute: 'status', values: ['x', 'y'] })
|
||||
})
|
||||
|
||||
test('Bulk-Create legt 3 Dokumente in einer Anfrage an', async () => {
|
||||
@@ -164,6 +167,146 @@ test('defaultLimit 25 liefert ohne limit-Abfrage 25 von 30', async () => {
|
||||
assert.equal((await listDocuments('termineOutbox', [...filter, Query.limit(5)])).length, 5)
|
||||
})
|
||||
|
||||
const seedOutbox = (feature, count, over = () => ({})) => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
aw.seed('termineOutbox', { $id: `t2_${feature}${String(i).padStart(2, '0')}`, featureId: feature, status: 'wartet', ...over(i) })
|
||||
}
|
||||
}
|
||||
const totalOf = async (queries) => {
|
||||
const url = `${rawDocs(aw, 'termineOutbox')}?${queries.map((q) => `queries[]=${encodeURIComponent(JSON.stringify(q))}`).join('&')}`
|
||||
return (await (await fetch(url)).json()).total
|
||||
}
|
||||
|
||||
test('select liefert nur die genannten Felder plus alle $-Felder', async () => {
|
||||
seedOutbox('f_sel', 2, (i) => ({ status: i === 0 ? 'wartet' : 'gesendet', empfaenger: 'a@example.invalid' }))
|
||||
const filter = [Query.equal('featureId', 'f_sel'), Query.orderAsc('$id')]
|
||||
const full = await listDocuments('termineOutbox', filter)
|
||||
assert.ok('featureId' in full[0] && 'status' in full[0] && 'empfaenger' in full[0])
|
||||
const slim = await listDocuments('termineOutbox', [...filter, Query.select(['status'])])
|
||||
assert.deepEqual(slim.map((d) => d.$id), ['t2_f_sel00', 't2_f_sel01'])
|
||||
assert.deepEqual(slim.map((d) => d.status), ['wartet', 'gesendet'])
|
||||
for (const doc of slim) {
|
||||
assert.deepEqual(
|
||||
Object.keys(doc).filter((key) => !key.startsWith('$')),
|
||||
['status']
|
||||
)
|
||||
// alle $-Felder bleiben, der interne Zaehler des Nachbaus nicht
|
||||
assert.deepEqual(Object.keys(doc).filter((key) => key.startsWith('$')).sort(), ['$collectionId', '$createdAt', '$id', '$updatedAt'])
|
||||
}
|
||||
// nur $id: die Abfrage filtert und sortiert trotzdem nach Feldern, die nicht ausgewaehlt sind
|
||||
const ids = await listDocuments('termineOutbox', [Query.equal('status', 'gesendet'), Query.equal('featureId', 'f_sel'), Query.select(['$id'])])
|
||||
assert.deepEqual(ids.map((d) => d.$id), ['t2_f_sel01'])
|
||||
assert.deepEqual(Object.keys(ids[0]).filter((key) => !key.startsWith('$')), [])
|
||||
// ein Feld, das das Dokument nicht hat, erscheint nicht
|
||||
const missing = await listDocuments('termineOutbox', [...filter, Query.select(['gibtEsNicht'])])
|
||||
assert.deepEqual(Object.keys(missing[0]).filter((key) => !key.startsWith('$')), [])
|
||||
// Einzelabruf bleibt vollstaendig
|
||||
assert.equal((await getDocument('termineOutbox', 't2_f_sel00')).featureId, 'f_sel')
|
||||
})
|
||||
|
||||
test('notEqual schliesst einen Wert oder alle Werte der Liste aus', async () => {
|
||||
seedOutbox('f_ne', 4, (i) => ({ status: ['wartet', 'gesendet', 'fehler', 'abgebrochen'][i] }))
|
||||
const filter = Query.equal('featureId', 'f_ne')
|
||||
const order = Query.orderAsc('$id')
|
||||
const without = async (value) => (await listDocuments('termineOutbox', [filter, Query.notEqual('status', value), order])).map((d) => d.status)
|
||||
assert.deepEqual(await without('wartet'), ['gesendet', 'fehler', 'abgebrochen'])
|
||||
assert.deepEqual(await without(['gesendet', 'fehler']), ['wartet', 'abgebrochen'])
|
||||
assert.deepEqual(await without(['wartet', 'gesendet', 'fehler', 'abgebrochen']), [])
|
||||
assert.deepEqual(await without(['gibtEsNicht']), ['wartet', 'gesendet', 'fehler', 'abgebrochen'])
|
||||
// zwei notEqual auf demselben Feld schneiden sich (beide Bedingungen muessen gelten)
|
||||
const both = await listDocuments('termineOutbox', [filter, Query.notEqual('status', 'wartet'), Query.notEqual('status', 'fehler'), order])
|
||||
assert.deepEqual(both.map((d) => d.status), ['gesendet', 'abgebrochen'])
|
||||
})
|
||||
|
||||
test('total zaehlt alle Treffer, auch ueber limit und cursorAfter hinaus; countDocuments liefert es', async () => {
|
||||
seedOutbox('f_total', 5)
|
||||
seedOutbox('f_total_fremd', 3)
|
||||
const filter = Query.equal('featureId', 'f_total')
|
||||
assert.equal(await totalOf([filter, Query.limit(2)]), 5)
|
||||
assert.equal(await totalOf([filter, Query.orderAsc('$id'), Query.cursorAfter('t2_f_total01'), Query.limit(2)]), 5)
|
||||
// das Cursor-Dokument zaehlt nur mit, wenn es zu den Filtern passt
|
||||
assert.equal(await totalOf([Query.equal('featureId', 'f_total'), Query.orderAsc('$id'), Query.cursorAfter('t2_f_total_fremd00'), Query.limit(2)]), 5)
|
||||
assert.equal(await totalOf([Query.equal('featureId', 'f_nirgends'), Query.limit(2)]), 0)
|
||||
assert.equal(await totalOf([filter]), 5, 'ohne limit gilt der Standard von 25, total bleibt die Trefferzahl')
|
||||
assert.equal(await countDocuments('termineOutbox', [filter]), 5)
|
||||
assert.equal(await countDocuments('termineOutbox', [filter, Query.notEqual('$id', ['t2_f_total00', 't2_f_total01'])]), 3)
|
||||
assert.equal(await countDocuments('termineOutbox', [Query.equal('featureId', 'f_nirgends')]), 0)
|
||||
// 30 Treffer ueber dem Standardlimit 25: total sagt 30, die Liste hat 25
|
||||
seedOutbox('f_total_dreissig', 30)
|
||||
assert.equal(await countDocuments('termineOutbox', [Query.equal('featureId', 'f_total_dreissig')]), 30)
|
||||
assert.equal((await listDocuments('termineOutbox', [Query.equal('featureId', 'f_total_dreissig')])).length, 25)
|
||||
})
|
||||
|
||||
test('countDocuments fragt mit limit(1) in einem Aufruf und liefert null ohne total', async () => {
|
||||
seedOutbox('f_count', 4)
|
||||
aw.resetListCalls()
|
||||
assert.equal(await countDocuments('termineOutbox', [Query.equal('featureId', 'f_count')]), 4)
|
||||
assert.deepEqual(aw.listCalls('termineOutbox'), [
|
||||
{
|
||||
collection: 'termineOutbox',
|
||||
queries: [
|
||||
{ method: 'equal', attribute: 'featureId', values: ['f_count'] },
|
||||
{ method: 'limit', values: [1] },
|
||||
],
|
||||
},
|
||||
])
|
||||
assert.equal(await countDocuments('termineOutbox'), await countDocuments('termineOutbox', []))
|
||||
// Antwort ohne total (oder mit einem Nicht-Zahlenwert): null, nicht 0 und nicht die Seitenlaenge
|
||||
const realFetch = globalThis.fetch
|
||||
const answer = (body) => () => Promise.resolve(new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } }))
|
||||
try {
|
||||
globalThis.fetch = answer({ documents: [{ $id: 'x' }] })
|
||||
assert.equal(await countDocuments('termineOutbox', []), null)
|
||||
globalThis.fetch = answer({ total: 'viele', documents: [] })
|
||||
assert.equal(await countDocuments('termineOutbox', []), null)
|
||||
globalThis.fetch = answer({ total: 7, documents: [{ $id: 'x' }] })
|
||||
assert.equal(await countDocuments('termineOutbox', []), 7)
|
||||
} finally {
|
||||
globalThis.fetch = realFetch
|
||||
}
|
||||
// ein Fehler der Abfrage wird nicht verschluckt
|
||||
aw.failNext({ collection: 'termineOutbox', method: 'GET', status: 500 })
|
||||
await assert.rejects(countDocuments('termineOutbox', []), (err) => err.status === 500)
|
||||
})
|
||||
|
||||
test('listCalls zeichnet nur Listenabfragen auf, geparst, je Collection filterbar', async () => {
|
||||
aw.resetListCalls()
|
||||
assert.deepEqual(aw.listCalls(), [])
|
||||
await createDocument('termineClaims', cell('t2_lc1', { featureId: 'f_lc' }), 't2_lc1')
|
||||
await createDocuments('termineClaims', [cell('t2_lc2', { featureId: 'f_lc' })])
|
||||
await getDocument('termineClaims', 't2_lc1')
|
||||
await updateDocument('termineClaims', 't2_lc1', { bookingId: 'b9' })
|
||||
assert.deepEqual(aw.listCalls(), [], 'Anlegen, Bulk, Einzelabruf und Aendern sind keine Listenabfragen')
|
||||
await listDocuments('termineClaims', [Query.equal('featureId', 'f_lc'), Query.orderAsc('$id'), Query.limit(3)])
|
||||
await listDocuments('termineBookings')
|
||||
await listAllDocuments('termineClaims', [Query.equal('featureId', 'f_lc')], { pageSize: 1 })
|
||||
assert.deepEqual(aw.listCalls('termineClaims').slice(0, 1), [
|
||||
{
|
||||
collection: 'termineClaims',
|
||||
queries: [
|
||||
{ method: 'equal', attribute: 'featureId', values: ['f_lc'] },
|
||||
{ method: 'orderAsc', attribute: '$id' },
|
||||
{ method: 'limit', values: [3] },
|
||||
],
|
||||
},
|
||||
])
|
||||
assert.deepEqual(aw.listCalls('termineBookings'), [{ collection: 'termineBookings', queries: [] }])
|
||||
assert.deepEqual(aw.listCalls().map((c) => c.collection), ['termineClaims', 'termineBookings', 'termineClaims', 'termineClaims', 'termineClaims'])
|
||||
assert.deepEqual(aw.listCalls('gibtEsNicht'), [])
|
||||
// listAllDocuments liest ueber Cursor weiter: 2 Treffer bei pageSize 1 = 3 Seiten, die zweite ab dem ersten Treffer
|
||||
const pages = aw.listCalls('termineClaims').slice(1)
|
||||
assert.equal(pages.length, 3)
|
||||
assert.equal(pages[0].queries.some((q) => q.method === 'cursorAfter'), false)
|
||||
assert.deepEqual(pages[1].queries.at(-1), { method: 'cursorAfter', values: ['t2_lc1'] })
|
||||
assert.deepEqual(pages[2].queries.at(-1), { method: 'cursorAfter', values: ['t2_lc2'] })
|
||||
// die Rueckgabe ist eine Kopie: Aendern wirkt nicht auf die Aufzeichnung
|
||||
aw.listCalls()[0].queries.length = 0
|
||||
assert.equal(aw.listCalls('termineClaims')[0].queries.length, 3)
|
||||
aw.resetListCalls()
|
||||
assert.deepEqual(aw.listCalls(), [])
|
||||
assert.deepEqual(aw.listCalls('termineClaims'), [])
|
||||
})
|
||||
|
||||
test('Datum mit Z kommt als +00:00 zurueck (Anlegen, Aendern, Bulk, Seed)', async () => {
|
||||
const made = await createDocument('termineBookings', { featureId: 'f_dt', startsAt: '2026-10-05T10:00:00Z', endsAt: '2026-10-05T12:30:00+02:00', status: 'x' }, 't2_dt1')
|
||||
assert.equal(made.startsAt, '2026-10-05T10:00:00.000+00:00')
|
||||
|
||||
Reference in new issue
Block a user