Termine-Outbox: notEqual mit genau einem Wert (Appwrite 1.8.1), nachrangige Mails kommen wieder dran
Auf der echten Appwrite 1.8.1 lehnt notEqual mit Liste die Abfrage ab (HTTP 400
general_query_invalid, "NotEqual queries require exactly one value"). Die Klasse normal der
Mail-Warteschlange schickte immer eine Liste, jeder Lauf warf dort, und die nachrangige Klasse
(G05, K20, K21) kam nie dran.
- appwriteAdmin: Query.notEqual nimmt genau einen Wert (eine Liste wirft einen TypeError, der
keinerVon nennt); neu keinerVon(attribute, werte) = je Wert ein notEqual (Appwrite schneidet
sie); dokumenteUrl() baut die Listen-URL ohne Netz, mit derselben Serialisierung wie die Anfrage.
- outbox: klassenFilter liefert fuer jede Klasse eine Liste von Abfragen (normal = sechs notEqual
auf art), die Ausschlussliste ist keinerVon('featureId', ...). AUSSCHLUSS_MAX 90 -> 30 (der nginx
antwortet in dieser Abfrageform ab 46 Funktions-IDs mit 414); der Kommentar "100 Werte je Abfrage"
entfaellt.
- Nachbau (appwriteMock): notEqual mit Liste oder ohne Wert => 400 wie Appwrite; ein Dokument mit
Feld null oder ohne Feld passt nicht zu notEqual (SQL-Semantik).
- Tests: Query-Helfer, keinerVon, dokumenteUrl, Nachbau (400, null), ein Lauf bedient alle drei
Klassen, Ausschlussliste hoechstens 30 und URL unter 6000 Zeichen im schlimmsten Fall.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af63b69ec1
commit
2c7cd26532
5 files changed
+196
-39
No files matched your search
@@ -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, countDocuments } = await import(
|
||||
const { Query, keinerVon, dokumenteUrl, createDocument, createDocuments, getDocument, updateDocument, listDocuments, listAllDocuments, countDocuments } = await import(
|
||||
'../server/services/appwriteAdmin.js'
|
||||
)
|
||||
|
||||
@@ -41,7 +41,36 @@ test('Query-Helfer bauen die Appwrite-Abfragen', () => {
|
||||
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'] })
|
||||
})
|
||||
|
||||
// Appwrite 1.8.1 (gemessen 2026-10-05): notEqual mit Liste => HTTP 400 "NotEqual queries require exactly one value."
|
||||
test('Query.notEqual nimmt genau einen Wert; eine Liste wirft einen TypeError, der keinerVon nennt', () => {
|
||||
assert.throws(() => Query.notEqual('status', ['x', 'y']), (err) => err instanceof TypeError && /keinerVon/.test(err.message))
|
||||
assert.throws(() => Query.notEqual('status', []), (err) => err instanceof TypeError && /keinerVon/.test(err.message))
|
||||
assert.throws(() => Query.notEqual('status', ['x']), TypeError, 'auch eine Liste mit einem Wert: der Aufrufer soll den Wert selbst uebergeben')
|
||||
})
|
||||
|
||||
test('keinerVon baut je Wert eine notEqual-Abfrage mit genau einem Wert (Appwrite schneidet sie)', () => {
|
||||
assert.deepEqual(keinerVon('status', ['x', 'y', 'z']), [
|
||||
{ method: 'notEqual', attribute: 'status', values: ['x'] },
|
||||
{ method: 'notEqual', attribute: 'status', values: ['y'] },
|
||||
{ method: 'notEqual', attribute: 'status', values: ['z'] },
|
||||
])
|
||||
assert.deepEqual(keinerVon('status', []), [])
|
||||
assert.deepEqual(keinerVon('status', new Set(['x'])), [{ method: 'notEqual', attribute: 'status', values: ['x'] }])
|
||||
assert.throws(() => keinerVon('status', [['x', 'y']]), TypeError, 'ein Eintrag ist ein einzelner Wert, keine Liste')
|
||||
})
|
||||
|
||||
test('dokumenteUrl: dieselbe Serialisierung wie die Anfrage an Appwrite (eine queries[] je Abfrage, URL-kodiert)', () => {
|
||||
const url = dokumenteUrl('termineOutbox', [Query.equal('status', 'wartet'), ...keinerVon('art', ['G01', 'G02'])])
|
||||
const parsed = new URL(url)
|
||||
assert.match(parsed.pathname, /\/collections\/termineOutbox\/documents$/)
|
||||
assert.deepEqual(parsed.searchParams.getAll('queries[]').map((q) => JSON.parse(q)), [
|
||||
{ method: 'equal', attribute: 'status', values: ['wartet'] },
|
||||
{ method: 'notEqual', attribute: 'art', values: ['G01'] },
|
||||
{ method: 'notEqual', attribute: 'art', values: ['G02'] },
|
||||
])
|
||||
assert.ok(url.includes('queries%5B%5D='), 'die Klammern stehen kodiert in der URL, so zaehlt der nginx sie')
|
||||
})
|
||||
|
||||
test('Bulk-Create legt 3 Dokumente in einer Anfrage an', async () => {
|
||||
@@ -204,20 +233,52 @@ test('select liefert nur die genannten Felder plus alle $-Felder', async () => {
|
||||
assert.equal((await getDocument('termineOutbox', 't2_f_sel00')).featureId, 'f_sel')
|
||||
})
|
||||
|
||||
test('notEqual schliesst einen Wert oder alle Werte der Liste aus', async () => {
|
||||
test('notEqual schliesst einen Wert aus, mehrere notEqual schneiden sich (keinerVon)', 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)
|
||||
const without = async (...werte) => (await listDocuments('termineOutbox', [filter, ...keinerVon('status', werte), 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)
|
||||
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'])
|
||||
assert.deepEqual(await without(), ['wartet', 'gesendet', 'fehler', 'abgebrochen'], 'ohne Werte keine Abfrage')
|
||||
// von Hand zwei notEqual auf demselben Feld: 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'])
|
||||
})
|
||||
|
||||
// Appwrite 1.8.1 (M1): HTTP 400 general_query_invalid; der Nachbau lehnt eine Liste genauso ab
|
||||
test('Nachbau: notEqual mit Liste (oder ohne Wert) => HTTP 400 wie Appwrite, auch ueber den rohen Pfad', async () => {
|
||||
seedOutbox('f_ne400', 2)
|
||||
const roh = (values) => JSON.stringify({ method: 'notEqual', attribute: 'status', values })
|
||||
for (const values of [['a', 'b'], ['a', 'b', 'c'], []]) {
|
||||
const antwort = await fetch(`${rawDocs(aw, 'termineOutbox')}?queries[]=${encodeURIComponent(roh(values))}`)
|
||||
assert.equal(antwort.status, 400, JSON.stringify(values))
|
||||
assert.deepEqual(await antwort.json(), { message: 'Invalid query: NotEqual queries require exactly one value.', code: 400, type: 'general_query_invalid' })
|
||||
}
|
||||
// durch appwriteAdmin: derselbe Fehler (status, type), kein Treffer
|
||||
await assert.rejects(
|
||||
listDocuments('termineOutbox', [roh(['a', 'b'])]),
|
||||
(err) => err.status === 400 && err.type === 'general_query_invalid' && /exactly one value/.test(err.message),
|
||||
)
|
||||
// ein Wert geht
|
||||
assert.equal((await listDocuments('termineOutbox', [roh(['a']), Query.equal('featureId', 'f_ne400')])).length, 2)
|
||||
})
|
||||
|
||||
// Appwrite 1.8.1 (M3): SQL-Semantik, ein Dokument ohne Wert (null oder nie gesetzt) passt zu notEqual nicht
|
||||
test('Nachbau: ein Dokument mit Feld null oder ohne Feld passt nicht zu notEqual', async () => {
|
||||
aw.seed('termineOutbox', { $id: 't2_f_nl00', featureId: 'f_nl', status: 'wartet' })
|
||||
aw.seed('termineOutbox', { $id: 't2_f_nl01', featureId: 'f_nl', status: null })
|
||||
aw.seed('termineOutbox', { $id: 't2_f_nl02', featureId: 'f_nl' })
|
||||
aw.seed('termineOutbox', { $id: 't2_f_nl03', featureId: 'f_nl', status: 'gesendet' })
|
||||
const ids = async (...queries) => (await listDocuments('termineOutbox', [Query.equal('featureId', 'f_nl'), ...queries, Query.orderAsc('$id')])).map((d) => d.$id)
|
||||
assert.deepEqual(await ids(Query.notEqual('status', 'wartet')), ['t2_f_nl03'], 'null und fehlend sind ausgeschlossen')
|
||||
assert.deepEqual(await ids(Query.notEqual('status', 'gibtEsNicht')), ['t2_f_nl00', 't2_f_nl03'])
|
||||
assert.deepEqual(await ids(...keinerVon('status', ['wartet', 'gesendet'])), [])
|
||||
assert.equal(await countDocuments('termineOutbox', [Query.equal('featureId', 'f_nl'), Query.notEqual('status', 'wartet')]), 1)
|
||||
})
|
||||
|
||||
test('total zaehlt alle Treffer, auch ueber limit und cursorAfter hinaus; countDocuments liefert es', async () => {
|
||||
seedOutbox('f_total', 5)
|
||||
seedOutbox('f_total_fremd', 3)
|
||||
@@ -229,7 +290,7 @@ test('total zaehlt alle Treffer, auch ueber limit und cursorAfter hinaus; countD
|
||||
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', [filter, ...keinerVon('$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)
|
||||
|
||||
Reference in new issue
Block a user