From c95fafe3d36f60c8a4a62d1c0098ae007d26f3db Mon Sep 17 00:00:00 2001 From: len360 Date: Mon, 27 Apr 2026 15:32:27 +0200 Subject: [PATCH 1/2] fix: handle excessively long URLs by performing local equality checks On patch event: 1. Fetch the selected document from the server 2. Compare the assumed master state with the server state locally 3. If there is a difference -> conflict detected, return the server state 4. Otherwise -> update the server state --- src/plugins/replication-supabase/helper.ts | 62 +--------------------- src/plugins/replication-supabase/index.ts | 40 +++++++------- test/replication-supabase.test.ts | 58 +++++++++++++++++++- 3 files changed, 77 insertions(+), 83 deletions(-) diff --git a/src/plugins/replication-supabase/helper.ts b/src/plugins/replication-supabase/helper.ts index 80d9d7ab3fc..00463f10fbe 100644 --- a/src/plugins/replication-supabase/helper.ts +++ b/src/plugins/replication-supabase/helper.ts @@ -1,63 +1,3 @@ -import { SupabaseClient } from '@supabase/supabase-js'; -import { RxDocumentData, RxJsonSchema, WithDeleted } from '../../types'; - export const POSTGRES_INSERT_CONFLICT_CODE = "23505"; export const DEFAULT_MODIFIED_FIELD = '_modified'; -export const DEFAULT_DELETED_FIELD = '_deleted'; - - -export function addDocEqualityToQuery( - jsonSchema: RxJsonSchema>, - deletedField: string, - modifiedField: string, - doc: WithDeleted, - query: any -) { - const ignoreKeys = new Set([ - modifiedField, - deletedField, - '_meta', - '_attachments', - '_rev' - ]); - - for (const key of Object.keys(doc)) { - if ( - ignoreKeys.has(key) - ) { - continue; - } - - const v = (doc as any)[key]; - const type = typeof v; - - if (type === "string" || type === "number") { - query = query.eq(key, v); - } else if (type === "boolean" || v === null) { - query = query.is(key, v); - } else if (type === 'undefined') { - query = query.is(key, null); - } else { - throw new Error(`unknown how to handle type: ${type}`) - } - } - - const schemaProps: Record = jsonSchema.properties; - for (const key of Object.keys(schemaProps)) { - if ( - ignoreKeys.has(key) || - Object.hasOwn(doc, key) - ) { - continue; - } - query = query.is(key, null); - } - - query = query.eq(deletedField, doc._deleted); - if (schemaProps[modifiedField]) { - query = query.eq(modifiedField, (doc as any)[modifiedField]); - } - - - return query; -} +export const DEFAULT_DELETED_FIELD = '_deleted'; \ No newline at end of file diff --git a/src/plugins/replication-supabase/index.ts b/src/plugins/replication-supabase/index.ts index 58c763a93d3..1d9040ca8da 100644 --- a/src/plugins/replication-supabase/index.ts +++ b/src/plugins/replication-supabase/index.ts @@ -16,8 +16,7 @@ import { Subject } from 'rxjs'; import { DEFAULT_DELETED_FIELD, DEFAULT_MODIFIED_FIELD, - POSTGRES_INSERT_CONFLICT_CODE, - addDocEqualityToQuery + POSTGRES_INSERT_CONFLICT_CODE } from './helper.ts'; import { ensureNotFalsy, flatClone, lastOfArray } from '../utils/index.ts'; @@ -197,29 +196,30 @@ export function replicateSupabase( // modified field will be set server-side delete toRow[modifiedField]; - let query = options.client - .from(options.tableName) - .update(toRow); + // fetch the current document state from the server + const docOnServer: WithDeleted = await fetchById(id); - query = addDocEqualityToQuery( - collection.schema.jsonSchema, - deletedField, - modifiedField, - assumedMasterState, - query - ); - - const { data, error } = await query.select(); - if (error) { - throw error; + if (!docOnServer) { + // the document does not exist on the server -> treat as conflict + return docOnServer; } + + const isSame = (Object.keys(assumedMasterState) as (keyof WithDeleted)[]) + .every((prop) => docOnServer[prop] === assumedMasterState[prop]) + + // check whether the server state matches the assumed master state + if (isSame) { + // no conflict -> proceed with the update + await options.client + .from(options.tableName) + .update(toRow) + .eq(primaryPath, id); - if (data && data.length > 0) { return; - } else { - // no match -> conflict - return await fetchById(id); } + + // conflict detected -> return the current server state + return docOnServer; } const conflicts: WithDeleted[] = []; diff --git a/test/replication-supabase.test.ts b/test/replication-supabase.test.ts index a555bbac165..111857d9a0b 100644 --- a/test/replication-supabase.test.ts +++ b/test/replication-supabase.test.ts @@ -5,7 +5,8 @@ import { ensureNotFalsy, addRxPlugin, RxCollection, - WithDeleted + WithDeleted, + RxJsonSchema } from '../plugins/core/index.mjs'; import { lastOfArray @@ -17,7 +18,8 @@ import { ensureReplicationHasNoErrors, SimpleHumanDocumentType, PrimaryHumanDocType, - runReplicationBaseTestSuite + runReplicationBaseTestSuite, + HumanDocumentType } from '../plugins/test-utils/index.mjs'; import { RxDBDevModePlugin } from '../plugins/dev-mode/index.mjs'; import config from './unit/config.ts'; @@ -480,6 +482,58 @@ describe('replication-supabase.test.ts', function () { await collection.database.close(); }); + + it('#7986 does not add all document fields as equality conditions in the PATCH request URL', async () => { + await cleanUpServer(); + + const customHumanSchemaWithoutPropertySizeLimits: RxJsonSchema = { + title: 'human schema without property size limits', + version: 0, + keyCompression: false, + type: 'object', + primaryKey: 'passportId', + properties: { + passportId: { + type: 'string', + maxLength: 100 + }, + firstName: { + type: 'string' + }, + lastName: { + type: 'string' + }, + age: { + type: 'integer', + } + }, + required: ['passportId'] + }; + + const collection = await humansCollection.createBySchema(customHumanSchemaWithoutPropertySizeLimits); + + const replicationState = replicateSupabase({ + tableName, + client: supabase, + replicationIdentifier: randomToken(10), + collection, + pull: { batchSize }, + push: { batchSize } + }); + ensureReplicationHasNoErrors(replicationState); + + const commonId = randomToken(10); + const doc = await collection.insert(schemaObjects.humanData(commonId, undefined, randomToken(40000))); + await replicationState.awaitInSync(); + + await doc.patch({ firstName: '0' }); + + const serverState = await getServerState(); + assert.strictEqual(serverState.length, 1); + + await collection.database.close(); + await cleanUpServer(); + }); }); describe('last', () => { From 0c983d2febb012f1ebb81b7bd1c5f0890ec27f28 Mon Sep 17 00:00:00 2001 From: len360 Date: Mon, 27 Apr 2026 16:59:19 +0200 Subject: [PATCH 2/2] fix: fix type checking issues. --- src/plugins/replication-supabase/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/replication-supabase/index.ts b/src/plugins/replication-supabase/index.ts index 1d9040ca8da..8769554a40c 100644 --- a/src/plugins/replication-supabase/index.ts +++ b/src/plugins/replication-supabase/index.ts @@ -184,7 +184,8 @@ export function replicateSupabase( assumedMasterState: WithDeleted ): Promise | undefined> { ensureNotFalsy(assumedMasterState); - const id = (doc as any)[primaryPath]; + const primaryKey: string = primaryPath; + const id = (doc as any)[primaryKey]; const toRow: Record = flatClone(doc); if (doc._deleted) { toRow[deletedField] = !!doc._deleted; @@ -213,7 +214,7 @@ export function replicateSupabase( await options.client .from(options.tableName) .update(toRow) - .eq(primaryPath, id); + .eq(primaryKey, id); return; }