@syncular/server 0.0.1-73 → 0.0.1-83

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.
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/blobs/adapters/database.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAIV,kBAAkB,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,IAAI,CACF,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAC3E,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAC7B,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;QAC9B,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG,IAAI,CAAC,CAAC;CACX;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAiDrE;AAQD,MAAM,WAAW,iCAAiC,CAChD,EAAE,SAAS,WAAW,GAAG,WAAW;IAEpC,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,WAAW,EAAE,eAAe,CAAC;CAC9B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gCAAgC,CAAC,EAAE,SAAS,WAAW,EACrE,OAAO,EAAE,iCAAiC,CAAC,EAAE,CAAC,GAC7C,kBAAkB,CAiGpB;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,SAAS,WAAW,EAC9D,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,IAAI,EAAE;IACJ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;CAClB,GACA,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,SAAS,WAAW,EAC/D,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBtE"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/blobs/adapters/database.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAIV,kBAAkB,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAE5C;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;OAKG;IACH,IAAI,CACF,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAC3E,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAC7B,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;QAC9B,SAAS,EAAE,MAAM,CAAC;KACnB,GAAG,IAAI,CAAC,CAAC;CACX;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CA8DrE;AAoBD,MAAM,WAAW,iCAAiC,CAChD,EAAE,SAAS,WAAW,GAAG,WAAW;IAEpC,+BAA+B;IAC/B,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,WAAW,EAAE,eAAe,CAAC;CAC9B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gCAAgC,CAAC,EAAE,SAAS,WAAW,EACrE,OAAO,EAAE,iCAAiC,CAAC,EAAE,CAAC,GAC7C,kBAAkB,CAiGpB;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,SAAS,WAAW,EAC9D,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,IAAI,EAAE;IACJ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;CAClB,GACA,OAAO,CAAC,IAAI,CAAC,CAkBf;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,SAAS,WAAW,EAC/D,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAoBtE"}
@@ -11,11 +11,21 @@ import { sql } from 'kysely';
11
11
  */
12
12
  export function createHmacTokenSigner(secret) {
13
13
  const encoder = new TextEncoder();
14
+ const keyPromise = crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
14
15
  async function hmacSign(data) {
15
- const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
16
+ const key = await keyPromise;
16
17
  const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
17
18
  return bufferToHex(new Uint8Array(signature));
18
19
  }
20
+ async function hmacVerify(data, signatureHex) {
21
+ const parsedSignature = hexToBuffer(signatureHex);
22
+ if (!parsedSignature)
23
+ return false;
24
+ const signature = new Uint8Array(parsedSignature.length);
25
+ signature.set(parsedSignature);
26
+ const key = await keyPromise;
27
+ return crypto.subtle.verify('HMAC', key, signature, encoder.encode(data));
28
+ }
19
29
  return {
20
30
  async sign(payload, _expiresIn) {
21
31
  const data = JSON.stringify(payload);
@@ -27,8 +37,8 @@ export function createHmacTokenSigner(secret) {
27
37
  const [dataB64, sig] = token.split('.');
28
38
  if (!dataB64 || !sig)
29
39
  return null;
30
- const expectedSig = await hmacSign(dataB64);
31
- if (sig !== expectedSig)
40
+ const isValidSig = await hmacVerify(dataB64, sig);
41
+ if (!isValidSig)
32
42
  return null;
33
43
  try {
34
44
  const data = JSON.parse(atob(dataB64));
@@ -47,6 +57,18 @@ function bufferToHex(buffer) {
47
57
  .map((b) => b.toString(16).padStart(2, '0'))
48
58
  .join('');
49
59
  }
60
+ function hexToBuffer(hex) {
61
+ if (hex.length === 0 || hex.length % 2 !== 0)
62
+ return null;
63
+ if (!/^[0-9a-f]+$/i.test(hex))
64
+ return null;
65
+ const out = new Uint8Array(hex.length / 2);
66
+ for (let i = 0; i < hex.length; i += 2) {
67
+ const pair = hex.slice(i, i + 2);
68
+ out[i / 2] = Number.parseInt(pair, 16);
69
+ }
70
+ return out;
71
+ }
50
72
  /**
51
73
  * Create a database blob storage adapter.
52
74
  *
@@ -1 +1 @@
1
- {"version":3,"file":"database.js","sourceRoot":"","sources":["../../../src/blobs/adapters/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../../src/blobs/adapters/database.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @syncular/server - Blob storage exports
3
3
  */
4
- export * from './adapters/database';
5
- export * from './adapters/s3';
6
- export * from './manager';
7
- export * from './migrate';
8
- export * from './types';
4
+ export * from './adapters/database.js';
5
+ export * from './adapters/s3.js';
6
+ export * from './manager.js';
7
+ export * from './migrate.js';
8
+ export * from './types.js';
9
9
  //# sourceMappingURL=index.js.map
@@ -5,7 +5,7 @@
5
5
  * database-specific sync dialect implementations.
6
6
  */
7
7
  import { sql } from 'kysely';
8
- import { coerceIsoString, coerceNumber, parseScopes } from './helpers';
8
+ import { coerceIsoString, coerceNumber, parseScopes } from './helpers.js';
9
9
  /**
10
10
  * Abstract base class for server sync dialects.
11
11
  *
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @syncular/server - Dialect exports
3
3
  */
4
- export * from './base';
5
- export * from './helpers';
6
- export * from './types';
4
+ export * from './base.js';
5
+ export * from './helpers.js';
6
+ export * from './types.js';
7
7
  //# sourceMappingURL=index.js.map
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Convenience helpers for implementing server table handlers.
5
5
  */
6
- export * from './conflict';
7
- export * from './emitted-change';
8
- export * from './paginate';
9
- export * from './scope-strings';
6
+ export * from './conflict.js';
7
+ export * from './emitted-change.js';
8
+ export * from './paginate.js';
9
+ export * from './scope-strings.js';
10
10
  //# sourceMappingURL=index.js.map
package/dist/index.js CHANGED
@@ -8,20 +8,20 @@
8
8
  * - blob/media storage
9
9
  */
10
10
  export * from '@syncular/core';
11
- export * from './blobs';
12
- export * from './clients';
13
- export * from './compaction';
14
- export * from './dialect';
15
- export * from './helpers';
16
- export * from './migrate';
17
- export * from './proxy';
18
- export * from './prune';
19
- export * from './pull';
20
- export * from './push';
21
- export * from './realtime';
22
- export * from './schema';
23
- export * from './shapes';
24
- export * from './snapshot-chunks';
25
- export * from './stats';
26
- export * from './subscriptions';
11
+ export * from './blobs/index.js';
12
+ export * from './clients.js';
13
+ export * from './compaction.js';
14
+ export * from './dialect/index.js';
15
+ export * from './helpers/index.js';
16
+ export * from './migrate.js';
17
+ export * from './proxy/index.js';
18
+ export * from './prune.js';
19
+ export * from './pull.js';
20
+ export * from './push.js';
21
+ export * from './realtime/index.js';
22
+ export * from './schema.js';
23
+ export * from './shapes/index.js';
24
+ export * from './snapshot-chunks.js';
25
+ export * from './stats.js';
26
+ export * from './subscriptions/index.js';
27
27
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/proxy/handler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAEjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAO5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,qBAAqB,CAAC,EAAE,SAAS,UAAU,GAAG,UAAU;IACvE,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,0BAA0B;IAC1B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,gDAAgD;IAChD,MAAM,EAAE,kBAAkB,CAAC;IAC3B,uCAAuC;IACvC,GAAG,EAAE,iBAAiB,CAAC;IACvB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,kDAAkD;IAClD,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAiDD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,SAAS,UAAU,EAC3D,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC,GAC9B,OAAO,CAAC,uBAAuB,CAAC,CAmDlC"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/proxy/handler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAEjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAQ5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,qBAAqB,CAAC,EAAE,SAAS,UAAU,GAAG,UAAU;IACvE,yCAAyC;IACzC,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IACf,0BAA0B;IAC1B,OAAO,EAAE,iBAAiB,CAAC;IAC3B,gDAAgD;IAChD,MAAM,EAAE,kBAAkB,CAAC;IAC3B,uCAAuC;IACvC,GAAG,EAAE,iBAAiB,CAAC;IACvB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,uBAAuB;IACtC,kDAAkD;IAClD,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;IACjB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAiDD;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,EAAE,SAAS,UAAU,EAC3D,IAAI,EAAE,qBAAqB,CAAC,EAAE,CAAC,GAC9B,OAAO,CAAC,uBAAuB,CAAC,CAyDlC"}
@@ -4,8 +4,8 @@
4
4
  * Executes proxied queries with automatic oplog generation for mutations.
5
5
  */
6
6
  import { sql } from 'kysely';
7
- import { appendReturning, detectMutation, hasReturningClause, } from './mutation-detector';
8
- import { createOplogEntries } from './oplog';
7
+ import { appendReturning, detectMutation, hasReturningClause, hasReturningWildcard, } from './mutation-detector.js';
8
+ import { createOplogEntries } from './oplog.js';
9
9
  /**
10
10
  * Build a raw SQL query with parameters using Kysely's sql helper.
11
11
  *
@@ -72,8 +72,11 @@ export async function executeProxyQuery(args) {
72
72
  };
73
73
  }
74
74
  // Mutation with registered shape - append RETURNING * and create oplog
75
- const needsReturning = !hasReturningClause(sqlQuery);
76
- const finalSql = needsReturning ? appendReturning(sqlQuery) : sqlQuery;
75
+ const hasReturning = hasReturningClause(sqlQuery);
76
+ if (hasReturning && !hasReturningWildcard(sqlQuery)) {
77
+ throw new Error(`Proxy mutation on synced table "${mutation.tableName}" must use RETURNING * (or omit RETURNING)`);
78
+ }
79
+ const finalSql = hasReturning ? sqlQuery : appendReturning(sqlQuery);
77
80
  const result = await buildRawQuery(finalSql, parameters).execute(db);
78
81
  const affectedRows = result.rows;
79
82
  if (affectedRows.length === 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/proxy/handler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../../src/proxy/handler.ts"],"names":[],"mappings":"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"}
@@ -4,11 +4,11 @@
4
4
  * Server-side proxy functionality for database access.
5
5
  */
6
6
  // Query execution
7
- export { executeProxyQuery, } from './handler';
7
+ export { executeProxyQuery, } from './handler.js';
8
8
  // Mutation detection
9
- export { detectMutation } from './mutation-detector';
9
+ export { detectMutation } from './mutation-detector.js';
10
10
  // Oplog creation
11
11
  // Registry
12
- export { ProxyTableRegistry } from './registry';
12
+ export { ProxyTableRegistry } from './registry.js';
13
13
  // Types
14
14
  //# sourceMappingURL=index.js.map
@@ -21,6 +21,10 @@ export declare function detectMutation(sql: string): DetectedMutation | null;
21
21
  * Check if SQL already has a RETURNING clause.
22
22
  */
23
23
  export declare function hasReturningClause(sql: string): boolean;
24
+ /**
25
+ * Check if SQL has a wildcard RETURNING clause (RETURNING * or alias.*).
26
+ */
27
+ export declare function hasReturningWildcard(sql: string): boolean;
24
28
  /**
25
29
  * Append RETURNING * to a mutation query if not already present.
26
30
  *
@@ -1 +1 @@
1
- {"version":3,"file":"mutation-detector.d.ts","sourceRoot":"","sources":["../../src/proxy/mutation-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAqCnE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQnD"}
1
+ {"version":3,"file":"mutation-detector.d.ts","sourceRoot":"","sources":["../../src/proxy/mutation-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAE7C,MAAM,WAAW,gBAAgB;IAC/B,qBAAqB;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB;AAyMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CA2BnE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQnD"}
@@ -3,6 +3,187 @@
3
3
  *
4
4
  * Detects whether a SQL query is a mutation (INSERT/UPDATE/DELETE).
5
5
  */
6
+ function isWordStart(ch) {
7
+ return /[A-Za-z_]/.test(ch);
8
+ }
9
+ function isWordPart(ch) {
10
+ return /[A-Za-z0-9_$]/.test(ch);
11
+ }
12
+ function skipLeadingNoise(sql) {
13
+ let index = 0;
14
+ while (index < sql.length) {
15
+ while (index < sql.length && /[\s;]/.test(sql[index])) {
16
+ index += 1;
17
+ }
18
+ if (sql.startsWith('--', index)) {
19
+ index += 2;
20
+ while (index < sql.length && sql[index] !== '\n') {
21
+ index += 1;
22
+ }
23
+ continue;
24
+ }
25
+ if (sql.startsWith('/*', index)) {
26
+ const end = sql.indexOf('*/', index + 2);
27
+ if (end === -1)
28
+ return '';
29
+ index = end + 2;
30
+ continue;
31
+ }
32
+ break;
33
+ }
34
+ return sql.slice(index);
35
+ }
36
+ function extractMainStatement(sql) {
37
+ const normalized = skipLeadingNoise(sql);
38
+ if (!normalized.toLowerCase().startsWith('with')) {
39
+ return normalized;
40
+ }
41
+ const lower = normalized.toLowerCase();
42
+ const rootKeywords = new Set(['insert', 'update', 'delete', 'select']);
43
+ let index = 0;
44
+ let depth = 0;
45
+ let inSingleQuote = false;
46
+ let inDoubleQuote = false;
47
+ let inLineComment = false;
48
+ let inBlockComment = false;
49
+ while (index < normalized.length) {
50
+ const ch = normalized[index];
51
+ const next = normalized[index + 1];
52
+ if (inLineComment) {
53
+ if (ch === '\n')
54
+ inLineComment = false;
55
+ index += 1;
56
+ continue;
57
+ }
58
+ if (inBlockComment) {
59
+ if (ch === '*' && next === '/') {
60
+ inBlockComment = false;
61
+ index += 2;
62
+ continue;
63
+ }
64
+ index += 1;
65
+ continue;
66
+ }
67
+ if (inSingleQuote) {
68
+ if (ch === "'" && next === "'") {
69
+ index += 2;
70
+ continue;
71
+ }
72
+ if (ch === "'")
73
+ inSingleQuote = false;
74
+ index += 1;
75
+ continue;
76
+ }
77
+ if (inDoubleQuote) {
78
+ if (ch === '"' && next === '"') {
79
+ index += 2;
80
+ continue;
81
+ }
82
+ if (ch === '"')
83
+ inDoubleQuote = false;
84
+ index += 1;
85
+ continue;
86
+ }
87
+ if (ch === '-' && next === '-') {
88
+ inLineComment = true;
89
+ index += 2;
90
+ continue;
91
+ }
92
+ if (ch === '/' && next === '*') {
93
+ inBlockComment = true;
94
+ index += 2;
95
+ continue;
96
+ }
97
+ if (ch === "'") {
98
+ inSingleQuote = true;
99
+ index += 1;
100
+ continue;
101
+ }
102
+ if (ch === '"') {
103
+ inDoubleQuote = true;
104
+ index += 1;
105
+ continue;
106
+ }
107
+ if (ch === '(') {
108
+ depth += 1;
109
+ index += 1;
110
+ continue;
111
+ }
112
+ if (ch === ')') {
113
+ if (depth > 0)
114
+ depth -= 1;
115
+ index += 1;
116
+ continue;
117
+ }
118
+ if (depth === 0 && isWordStart(ch)) {
119
+ const tokenStart = index;
120
+ index += 1;
121
+ while (index < normalized.length && isWordPart(normalized[index])) {
122
+ index += 1;
123
+ }
124
+ const token = lower.slice(tokenStart, index);
125
+ if (token !== 'with' &&
126
+ token !== 'recursive' &&
127
+ rootKeywords.has(token)) {
128
+ return normalized.slice(tokenStart);
129
+ }
130
+ continue;
131
+ }
132
+ index += 1;
133
+ }
134
+ return normalized;
135
+ }
136
+ function parseIdentifier(input, startIndex) {
137
+ let index = startIndex;
138
+ while (index < input.length && /\s/.test(input[index])) {
139
+ index += 1;
140
+ }
141
+ if (index >= input.length)
142
+ return null;
143
+ if (input[index] === '"') {
144
+ index += 1;
145
+ let value = '';
146
+ while (index < input.length) {
147
+ const ch = input[index];
148
+ if (ch === '"' && input[index + 1] === '"') {
149
+ value += '"';
150
+ index += 2;
151
+ continue;
152
+ }
153
+ if (ch === '"') {
154
+ index += 1;
155
+ return { name: value, nextIndex: index };
156
+ }
157
+ value += ch;
158
+ index += 1;
159
+ }
160
+ return null;
161
+ }
162
+ if (!isWordStart(input[index]))
163
+ return null;
164
+ const first = index;
165
+ index += 1;
166
+ while (index < input.length && isWordPart(input[index])) {
167
+ index += 1;
168
+ }
169
+ return { name: input.slice(first, index), nextIndex: index };
170
+ }
171
+ function parseTargetTable(input) {
172
+ const first = parseIdentifier(input, 0);
173
+ if (!first)
174
+ return null;
175
+ let index = first.nextIndex;
176
+ while (index < input.length && /\s/.test(input[index])) {
177
+ index += 1;
178
+ }
179
+ if (input[index] !== '.') {
180
+ return first.name;
181
+ }
182
+ const second = parseIdentifier(input, index + 1);
183
+ if (!second)
184
+ return null;
185
+ return second.name;
186
+ }
6
187
  /**
7
188
  * Detect if a SQL query is a mutation and extract table info.
8
189
  *
@@ -10,30 +191,25 @@
10
191
  * @returns Mutation info if detected, null for read queries
11
192
  */
12
193
  export function detectMutation(sql) {
13
- const trimmed = sql.trim();
14
- // INSERT INTO [schema.]table
15
- const insertMatch = trimmed.match(/^\s*INSERT\s+INTO\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?/i);
16
- if (insertMatch) {
17
- return {
18
- operation: 'upsert',
19
- tableName: insertMatch[2],
20
- };
21
- }
22
- // UPDATE [schema.]table
23
- const updateMatch = trimmed.match(/^\s*UPDATE\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?/i);
24
- if (updateMatch) {
25
- return {
26
- operation: 'upsert',
27
- tableName: updateMatch[2],
28
- };
29
- }
30
- // DELETE FROM [schema.]table
31
- const deleteMatch = trimmed.match(/^\s*DELETE\s+FROM\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?/i);
32
- if (deleteMatch) {
33
- return {
34
- operation: 'delete',
35
- tableName: deleteMatch[2],
36
- };
194
+ const statement = extractMainStatement(sql).trimStart();
195
+ const lower = statement.toLowerCase();
196
+ if (lower.startsWith('insert')) {
197
+ const tableName = parseTargetTable(statement.replace(/^insert\s+into\s+/i, ''));
198
+ if (!tableName)
199
+ return null;
200
+ return { operation: 'upsert', tableName };
201
+ }
202
+ if (lower.startsWith('update')) {
203
+ const tableName = parseTargetTable(statement.replace(/^update\s+/i, ''));
204
+ if (!tableName)
205
+ return null;
206
+ return { operation: 'upsert', tableName };
207
+ }
208
+ if (lower.startsWith('delete')) {
209
+ const tableName = parseTargetTable(statement.replace(/^delete\s+from\s+/i, ''));
210
+ if (!tableName)
211
+ return null;
212
+ return { operation: 'delete', tableName };
37
213
  }
38
214
  return null;
39
215
  }
@@ -44,6 +220,15 @@ export function hasReturningClause(sql) {
44
220
  // Simple check - look for RETURNING keyword not in a string
45
221
  return /\bRETURNING\b/i.test(sql);
46
222
  }
223
+ /**
224
+ * Check if SQL has a wildcard RETURNING clause (RETURNING * or alias.*).
225
+ */
226
+ export function hasReturningWildcard(sql) {
227
+ const match = sql.match(/\bRETURNING\b([\s\S]*)$/i);
228
+ if (!match)
229
+ return false;
230
+ return /(^|,)\s*(?:[A-Za-z_][A-Za-z0-9_$]*\.)?\*/i.test(match[1]);
231
+ }
47
232
  /**
48
233
  * Append RETURNING * to a mutation query if not already present.
49
234
  *
@@ -1 +1 @@
1
- {"version":3,"file":"mutation-detector.js","sourceRoot":"","sources":["../../src/proxy/mutation-detector.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAA2B;IACnE,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,6BAA6B;IAC7B,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAC/B,4DAA4D,CAC7D,CAAC;IACF,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO;YACL,SAAS,EAAE,QAAQ;YACnB,SAAS,EAAE,WAAW,CAAC,CAAC,CAAE;SAC3B,CAAC;IACJ,CAAC;IAED,wBAAwB;IACxB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAC/B,qDAAqD,CACtD,CAAC;IACF,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO;YACL,SAAS,EAAE,QAAQ;YACnB,SAAS,EAAE,WAAW,CAAC,CAAC,CAAE;SAC3B,CAAC;IACJ,CAAC;IAED,6BAA6B;IAC7B,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAC/B,4DAA4D,CAC7D,CAAC;IACF,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO;YACL,SAAS,EAAE,QAAQ;YACnB,SAAS,EAAE,WAAW,CAAC,CAAC,CAAE;SAC3B,CAAC;IACJ,CAAC;IAED,OAAO,IAAI,CAAC;AAAA,CACb;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAW;IACvD,4DAA4D;IAC5D,OAAO,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA,CACnC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAU;IACnD,IAAI,kBAAkB,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC;IACb,CAAC;IAED,uCAAuC;IACvC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAChD,OAAO,GAAG,OAAO,cAAc,CAAC;AAAA,CACjC"}
1
+ {"version":3,"file":"mutation-detector.js","sourceRoot":"","sources":["../../src/proxy/mutation-detector.ts"],"names":[],"mappings":"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"}
package/dist/pull.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { promisify } from 'node:util';
3
3
  import { gzip, gzipSync } from 'node:zlib';
4
- import { insertSnapshotChunk, readSnapshotChunkRefByPageKey, } from './snapshot-chunks';
5
- import { resolveEffectiveScopesForSubscriptions } from './subscriptions/resolve';
4
+ import { insertSnapshotChunk, readSnapshotChunkRefByPageKey, } from './snapshot-chunks.js';
5
+ import { resolveEffectiveScopesForSubscriptions } from './subscriptions/resolve.js';
6
6
  const gzipAsync = promisify(gzip);
7
7
  const ASYNC_GZIP_MIN_BYTES = 64 * 1024;
8
8
  async function compressSnapshotNdjson(ndjson) {
@@ -1,2 +1,2 @@
1
- export { InMemorySyncRealtimeBroadcaster } from './in-memory';
1
+ export { InMemorySyncRealtimeBroadcaster } from './in-memory.js';
2
2
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- export * from './create-handler';
2
- export * from './registry';
3
- export * from './types';
1
+ export * from './create-handler.js';
2
+ export * from './registry.js';
3
+ export * from './types.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Stores snapshot chunk bodies in S3/R2/MinIO with metadata in database.
5
5
  */
6
- import { createDbMetadataChunkStorage } from '../db-metadata';
6
+ import { createDbMetadataChunkStorage } from '../db-metadata.js';
7
7
  /**
8
8
  * Create S3-compatible snapshot chunk storage.
9
9
  *
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Separates chunk metadata (database) from body content (blob storage).
5
5
  */
6
- export * from './adapters/s3';
7
- export * from './db-metadata';
8
- export * from './types';
6
+ export * from './adapters/s3.js';
7
+ export * from './db-metadata.js';
8
+ export * from './types.js';
9
9
  //# sourceMappingURL=index.js.map
@@ -1,2 +1,2 @@
1
- export * from './resolve';
1
+ export * from './resolve.js';
2
2
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/server",
3
- "version": "0.0.1-73",
3
+ "version": "0.0.1-83",
4
4
  "description": "Server-side sync engine with push/pull, pruning, and snapshot support",
5
5
  "license": "MIT",
6
6
  "author": "Benjamin Kniffler",
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createHmacTokenSigner } from './database';
3
+
4
+ describe('createHmacTokenSigner', () => {
5
+ test('verifies valid signed tokens', async () => {
6
+ const signer = createHmacTokenSigner('test-secret');
7
+ const payload = {
8
+ hash: 'sha256:abc',
9
+ action: 'upload' as const,
10
+ expiresAt: Date.now() + 60_000,
11
+ };
12
+
13
+ const token = await signer.sign(payload, 60);
14
+ const decoded = await signer.verify(token);
15
+
16
+ expect(decoded).toEqual(payload);
17
+ });
18
+
19
+ test('rejects tampered signatures', async () => {
20
+ const signer = createHmacTokenSigner('test-secret');
21
+ const payload = {
22
+ hash: 'sha256:def',
23
+ action: 'download' as const,
24
+ expiresAt: Date.now() + 60_000,
25
+ };
26
+
27
+ const token = await signer.sign(payload, 60);
28
+ const [data, sig] = token.split('.');
29
+ if (!data || !sig) {
30
+ throw new Error('Expected signed token with payload and signature');
31
+ }
32
+ const replacement = sig.endsWith('0') ? '1' : '0';
33
+ const tamperedSig = `${sig.slice(0, -1)}${replacement}`;
34
+ const tamperedToken = `${data}.${tamperedSig}`;
35
+
36
+ expect(await signer.verify(tamperedToken)).toBeNull();
37
+ });
38
+
39
+ test('rejects malformed hex signatures', async () => {
40
+ const signer = createHmacTokenSigner('test-secret');
41
+ const payload = {
42
+ hash: 'sha256:ghi',
43
+ action: 'upload' as const,
44
+ expiresAt: Date.now() + 60_000,
45
+ };
46
+
47
+ const token = await signer.sign(payload, 60);
48
+ const [data] = token.split('.');
49
+ if (!data) {
50
+ throw new Error('Expected signed token payload segment');
51
+ }
52
+
53
+ expect(await signer.verify(`${data}.not-hex-signature`)).toBeNull();
54
+ });
55
+
56
+ test('rejects expired tokens', async () => {
57
+ const signer = createHmacTokenSigner('test-secret');
58
+ const payload = {
59
+ hash: 'sha256:jkl',
60
+ action: 'upload' as const,
61
+ expiresAt: Date.now() - 1,
62
+ };
63
+
64
+ const token = await signer.sign(payload, 60);
65
+ expect(await signer.verify(token)).toBeNull();
66
+ });
67
+ });
@@ -46,15 +46,16 @@ export interface BlobTokenSigner {
46
46
  */
47
47
  export function createHmacTokenSigner(secret: string): BlobTokenSigner {
48
48
  const encoder = new TextEncoder();
49
+ const keyPromise = crypto.subtle.importKey(
50
+ 'raw',
51
+ encoder.encode(secret),
52
+ { name: 'HMAC', hash: 'SHA-256' },
53
+ false,
54
+ ['sign', 'verify']
55
+ );
49
56
 
50
57
  async function hmacSign(data: string): Promise<string> {
51
- const key = await crypto.subtle.importKey(
52
- 'raw',
53
- encoder.encode(secret),
54
- { name: 'HMAC', hash: 'SHA-256' },
55
- false,
56
- ['sign']
57
- );
58
+ const key = await keyPromise;
58
59
  const signature = await crypto.subtle.sign(
59
60
  'HMAC',
60
61
  key,
@@ -63,6 +64,18 @@ export function createHmacTokenSigner(secret: string): BlobTokenSigner {
63
64
  return bufferToHex(new Uint8Array(signature));
64
65
  }
65
66
 
67
+ async function hmacVerify(
68
+ data: string,
69
+ signatureHex: string
70
+ ): Promise<boolean> {
71
+ const parsedSignature = hexToBuffer(signatureHex);
72
+ if (!parsedSignature) return false;
73
+ const signature = new Uint8Array(parsedSignature.length);
74
+ signature.set(parsedSignature);
75
+ const key = await keyPromise;
76
+ return crypto.subtle.verify('HMAC', key, signature, encoder.encode(data));
77
+ }
78
+
66
79
  return {
67
80
  async sign(payload, _expiresIn) {
68
81
  const data = JSON.stringify(payload);
@@ -75,8 +88,8 @@ export function createHmacTokenSigner(secret: string): BlobTokenSigner {
75
88
  const [dataB64, sig] = token.split('.');
76
89
  if (!dataB64 || !sig) return null;
77
90
 
78
- const expectedSig = await hmacSign(dataB64);
79
- if (sig !== expectedSig) return null;
91
+ const isValidSig = await hmacVerify(dataB64, sig);
92
+ if (!isValidSig) return null;
80
93
 
81
94
  try {
82
95
  const data = JSON.parse(atob(dataB64)) as {
@@ -101,6 +114,18 @@ function bufferToHex(buffer: Uint8Array): string {
101
114
  .join('');
102
115
  }
103
116
 
117
+ function hexToBuffer(hex: string): Uint8Array | null {
118
+ if (hex.length === 0 || hex.length % 2 !== 0) return null;
119
+ if (!/^[0-9a-f]+$/i.test(hex)) return null;
120
+
121
+ const out = new Uint8Array(hex.length / 2);
122
+ for (let i = 0; i < hex.length; i += 2) {
123
+ const pair = hex.slice(i, i + 2);
124
+ out[i / 2] = Number.parseInt(pair, 16);
125
+ }
126
+ return out;
127
+ }
128
+
104
129
  export interface DatabaseBlobStorageAdapterOptions<
105
130
  DB extends SyncBlobsDb = SyncBlobsDb,
106
131
  > {
@@ -0,0 +1,120 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
+ import type { Kysely } from 'kysely';
3
+ import { createBunSqliteDb } from '../../../dialect-bun-sqlite/src';
4
+ import { createSqliteServerDialect } from '../../../server-dialect-sqlite/src';
5
+ import { ensureSyncSchema } from '../migrate';
6
+ import type { SyncCoreDb } from '../schema';
7
+ import { executeProxyQuery } from './handler';
8
+ import { ProxyTableRegistry } from './registry';
9
+
10
+ interface TasksTable {
11
+ id: string;
12
+ user_id: string;
13
+ title: string;
14
+ server_version: number;
15
+ }
16
+
17
+ interface ProxyTestDb extends SyncCoreDb {
18
+ tasks: TasksTable;
19
+ }
20
+
21
+ describe('executeProxyQuery', () => {
22
+ let db: Kysely<ProxyTestDb>;
23
+ const dialect = createSqliteServerDialect();
24
+ const shapes = new ProxyTableRegistry().register({
25
+ table: 'tasks',
26
+ computeScopes: (row) => ({
27
+ user_id: String(row.user_id),
28
+ }),
29
+ });
30
+
31
+ beforeEach(async () => {
32
+ db = createBunSqliteDb<ProxyTestDb>({ path: ':memory:' });
33
+ await ensureSyncSchema(db, dialect);
34
+
35
+ await db.schema
36
+ .createTable('tasks')
37
+ .addColumn('id', 'text', (col) => col.primaryKey())
38
+ .addColumn('user_id', 'text', (col) => col.notNull())
39
+ .addColumn('title', 'text', (col) => col.notNull())
40
+ .addColumn('server_version', 'integer', (col) => col.notNull())
41
+ .execute();
42
+
43
+ await db
44
+ .insertInto('tasks')
45
+ .values({
46
+ id: 't1',
47
+ user_id: 'u1',
48
+ title: 'old title',
49
+ server_version: 1,
50
+ })
51
+ .execute();
52
+ });
53
+
54
+ afterEach(async () => {
55
+ await db.destroy();
56
+ });
57
+
58
+ it('tracks comment-prefixed mutations in the sync oplog', async () => {
59
+ const result = await executeProxyQuery({
60
+ db,
61
+ dialect,
62
+ shapes,
63
+ ctx: { actorId: 'actor-1', clientId: 'proxy-client-1' },
64
+ sqlQuery:
65
+ '/* admin */ UPDATE tasks SET title = $1, server_version = server_version + 1 WHERE id = $2',
66
+ parameters: ['new title', 't1'],
67
+ });
68
+
69
+ expect(result.rowCount).toBe(1);
70
+ expect(result.commitSeq).toBeGreaterThan(0);
71
+
72
+ const commitCount = await db
73
+ .selectFrom('sync_commits')
74
+ .select(({ fn }) => fn.countAll().as('count'))
75
+ .executeTakeFirstOrThrow();
76
+ expect(Number(commitCount.count)).toBe(1);
77
+
78
+ const changeCount = await db
79
+ .selectFrom('sync_changes')
80
+ .select(({ fn }) => fn.countAll().as('count'))
81
+ .executeTakeFirstOrThrow();
82
+ expect(Number(changeCount.count)).toBe(1);
83
+
84
+ const updated = await db
85
+ .selectFrom('tasks')
86
+ .select(['title', 'server_version'])
87
+ .where('id', '=', 't1')
88
+ .executeTakeFirstOrThrow();
89
+ expect(updated.title).toBe('new title');
90
+ expect(updated.server_version).toBe(2);
91
+ });
92
+
93
+ it('rejects non-wildcard RETURNING on synced-table mutations', async () => {
94
+ await expect(
95
+ executeProxyQuery({
96
+ db,
97
+ dialect,
98
+ shapes,
99
+ ctx: { actorId: 'actor-1', clientId: 'proxy-client-1' },
100
+ sqlQuery: 'UPDATE tasks SET title = $1 WHERE id = $2 RETURNING id',
101
+ parameters: ['blocked title', 't1'],
102
+ })
103
+ ).rejects.toThrow(
104
+ 'Proxy mutation on synced table "tasks" must use RETURNING * (or omit RETURNING)'
105
+ );
106
+
107
+ const commitCount = await db
108
+ .selectFrom('sync_commits')
109
+ .select(({ fn }) => fn.countAll().as('count'))
110
+ .executeTakeFirstOrThrow();
111
+ expect(Number(commitCount.count)).toBe(0);
112
+
113
+ const row = await db
114
+ .selectFrom('tasks')
115
+ .select(['title'])
116
+ .where('id', '=', 't1')
117
+ .executeTakeFirstOrThrow();
118
+ expect(row.title).toBe('old title');
119
+ });
120
+ });
@@ -12,6 +12,7 @@ import {
12
12
  appendReturning,
13
13
  detectMutation,
14
14
  hasReturningClause,
15
+ hasReturningWildcard,
15
16
  } from './mutation-detector';
16
17
  import { createOplogEntries } from './oplog';
17
18
  import type { ProxyTableRegistry } from './registry';
@@ -122,8 +123,14 @@ export async function executeProxyQuery<DB extends SyncCoreDb>(
122
123
  }
123
124
 
124
125
  // Mutation with registered shape - append RETURNING * and create oplog
125
- const needsReturning = !hasReturningClause(sqlQuery);
126
- const finalSql = needsReturning ? appendReturning(sqlQuery) : sqlQuery;
126
+ const hasReturning = hasReturningClause(sqlQuery);
127
+ if (hasReturning && !hasReturningWildcard(sqlQuery)) {
128
+ throw new Error(
129
+ `Proxy mutation on synced table "${mutation.tableName}" must use RETURNING * (or omit RETURNING)`
130
+ );
131
+ }
132
+
133
+ const finalSql = hasReturning ? sqlQuery : appendReturning(sqlQuery);
127
134
 
128
135
  const result = await buildRawQuery(finalSql, parameters).execute(db);
129
136
  const affectedRows = result.rows as Record<string, unknown>[];
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import {
3
+ appendReturning,
4
+ detectMutation,
5
+ hasReturningWildcard,
6
+ } from './mutation-detector';
7
+
8
+ describe('detectMutation', () => {
9
+ it('detects comment-prefixed update statements', () => {
10
+ const detected = detectMutation(`
11
+ /* admin tooling */
12
+ UPDATE tasks
13
+ SET title = 'updated'
14
+ WHERE id = 't1'
15
+ `);
16
+
17
+ expect(detected).toEqual({
18
+ operation: 'upsert',
19
+ tableName: 'tasks',
20
+ });
21
+ });
22
+
23
+ it('detects cte-prefixed update statements', () => {
24
+ const detected = detectMutation(`
25
+ WITH touched AS (
26
+ SELECT id FROM tasks WHERE id = 't1'
27
+ )
28
+ UPDATE tasks
29
+ SET title = 'updated'
30
+ WHERE id IN (SELECT id FROM touched)
31
+ `);
32
+
33
+ expect(detected).toEqual({
34
+ operation: 'upsert',
35
+ tableName: 'tasks',
36
+ });
37
+ });
38
+
39
+ it('returns null for cte-prefixed read queries', () => {
40
+ const detected = detectMutation(`
41
+ WITH filtered AS (
42
+ SELECT id FROM tasks WHERE user_id = 'u1'
43
+ )
44
+ SELECT * FROM filtered
45
+ `);
46
+
47
+ expect(detected).toBeNull();
48
+ });
49
+ });
50
+
51
+ describe('returning helpers', () => {
52
+ it('recognizes wildcard RETURNING clauses', () => {
53
+ expect(
54
+ hasReturningWildcard('UPDATE tasks SET title = $1 RETURNING *')
55
+ ).toBe(true);
56
+ expect(
57
+ hasReturningWildcard(
58
+ 'UPDATE tasks SET title = $1 RETURNING tasks.id, tasks.*'
59
+ )
60
+ ).toBe(true);
61
+ expect(
62
+ hasReturningWildcard('UPDATE tasks SET title = $1 RETURNING id')
63
+ ).toBe(false);
64
+ });
65
+
66
+ it('appends RETURNING * when missing', () => {
67
+ expect(appendReturning('UPDATE tasks SET title = $1')).toBe(
68
+ 'UPDATE tasks SET title = $1 RETURNING *'
69
+ );
70
+ });
71
+ });
@@ -13,6 +13,205 @@ export interface DetectedMutation {
13
13
  tableName: string;
14
14
  }
15
15
 
16
+ function isWordStart(ch: string): boolean {
17
+ return /[A-Za-z_]/.test(ch);
18
+ }
19
+
20
+ function isWordPart(ch: string): boolean {
21
+ return /[A-Za-z0-9_$]/.test(ch);
22
+ }
23
+
24
+ function skipLeadingNoise(sql: string): string {
25
+ let index = 0;
26
+ while (index < sql.length) {
27
+ while (index < sql.length && /[\s;]/.test(sql[index]!)) {
28
+ index += 1;
29
+ }
30
+
31
+ if (sql.startsWith('--', index)) {
32
+ index += 2;
33
+ while (index < sql.length && sql[index] !== '\n') {
34
+ index += 1;
35
+ }
36
+ continue;
37
+ }
38
+
39
+ if (sql.startsWith('/*', index)) {
40
+ const end = sql.indexOf('*/', index + 2);
41
+ if (end === -1) return '';
42
+ index = end + 2;
43
+ continue;
44
+ }
45
+
46
+ break;
47
+ }
48
+
49
+ return sql.slice(index);
50
+ }
51
+
52
+ function extractMainStatement(sql: string): string {
53
+ const normalized = skipLeadingNoise(sql);
54
+ if (!normalized.toLowerCase().startsWith('with')) {
55
+ return normalized;
56
+ }
57
+
58
+ const lower = normalized.toLowerCase();
59
+ const rootKeywords = new Set(['insert', 'update', 'delete', 'select']);
60
+
61
+ let index = 0;
62
+ let depth = 0;
63
+ let inSingleQuote = false;
64
+ let inDoubleQuote = false;
65
+ let inLineComment = false;
66
+ let inBlockComment = false;
67
+
68
+ while (index < normalized.length) {
69
+ const ch = normalized[index]!;
70
+ const next = normalized[index + 1];
71
+
72
+ if (inLineComment) {
73
+ if (ch === '\n') inLineComment = false;
74
+ index += 1;
75
+ continue;
76
+ }
77
+ if (inBlockComment) {
78
+ if (ch === '*' && next === '/') {
79
+ inBlockComment = false;
80
+ index += 2;
81
+ continue;
82
+ }
83
+ index += 1;
84
+ continue;
85
+ }
86
+ if (inSingleQuote) {
87
+ if (ch === "'" && next === "'") {
88
+ index += 2;
89
+ continue;
90
+ }
91
+ if (ch === "'") inSingleQuote = false;
92
+ index += 1;
93
+ continue;
94
+ }
95
+ if (inDoubleQuote) {
96
+ if (ch === '"' && next === '"') {
97
+ index += 2;
98
+ continue;
99
+ }
100
+ if (ch === '"') inDoubleQuote = false;
101
+ index += 1;
102
+ continue;
103
+ }
104
+
105
+ if (ch === '-' && next === '-') {
106
+ inLineComment = true;
107
+ index += 2;
108
+ continue;
109
+ }
110
+ if (ch === '/' && next === '*') {
111
+ inBlockComment = true;
112
+ index += 2;
113
+ continue;
114
+ }
115
+ if (ch === "'") {
116
+ inSingleQuote = true;
117
+ index += 1;
118
+ continue;
119
+ }
120
+ if (ch === '"') {
121
+ inDoubleQuote = true;
122
+ index += 1;
123
+ continue;
124
+ }
125
+ if (ch === '(') {
126
+ depth += 1;
127
+ index += 1;
128
+ continue;
129
+ }
130
+ if (ch === ')') {
131
+ if (depth > 0) depth -= 1;
132
+ index += 1;
133
+ continue;
134
+ }
135
+
136
+ if (depth === 0 && isWordStart(ch)) {
137
+ const tokenStart = index;
138
+ index += 1;
139
+ while (index < normalized.length && isWordPart(normalized[index]!)) {
140
+ index += 1;
141
+ }
142
+ const token = lower.slice(tokenStart, index);
143
+ if (
144
+ token !== 'with' &&
145
+ token !== 'recursive' &&
146
+ rootKeywords.has(token)
147
+ ) {
148
+ return normalized.slice(tokenStart);
149
+ }
150
+ continue;
151
+ }
152
+
153
+ index += 1;
154
+ }
155
+
156
+ return normalized;
157
+ }
158
+
159
+ function parseIdentifier(
160
+ input: string,
161
+ startIndex: number
162
+ ): { name: string; nextIndex: number } | null {
163
+ let index = startIndex;
164
+ while (index < input.length && /\s/.test(input[index]!)) {
165
+ index += 1;
166
+ }
167
+ if (index >= input.length) return null;
168
+
169
+ if (input[index] === '"') {
170
+ index += 1;
171
+ let value = '';
172
+ while (index < input.length) {
173
+ const ch = input[index]!;
174
+ if (ch === '"' && input[index + 1] === '"') {
175
+ value += '"';
176
+ index += 2;
177
+ continue;
178
+ }
179
+ if (ch === '"') {
180
+ index += 1;
181
+ return { name: value, nextIndex: index };
182
+ }
183
+ value += ch;
184
+ index += 1;
185
+ }
186
+ return null;
187
+ }
188
+
189
+ if (!isWordStart(input[index]!)) return null;
190
+ const first = index;
191
+ index += 1;
192
+ while (index < input.length && isWordPart(input[index]!)) {
193
+ index += 1;
194
+ }
195
+ return { name: input.slice(first, index), nextIndex: index };
196
+ }
197
+
198
+ function parseTargetTable(input: string): string | null {
199
+ const first = parseIdentifier(input, 0);
200
+ if (!first) return null;
201
+
202
+ let index = first.nextIndex;
203
+ while (index < input.length && /\s/.test(input[index]!)) {
204
+ index += 1;
205
+ }
206
+ if (input[index] !== '.') {
207
+ return first.name;
208
+ }
209
+
210
+ const second = parseIdentifier(input, index + 1);
211
+ if (!second) return null;
212
+ return second.name;
213
+ }
214
+
16
215
  /**
17
216
  * Detect if a SQL query is a mutation and extract table info.
18
217
  *
@@ -20,39 +219,29 @@ export interface DetectedMutation {
20
219
  * @returns Mutation info if detected, null for read queries
21
220
  */
22
221
  export function detectMutation(sql: string): DetectedMutation | null {
23
- const trimmed = sql.trim();
24
-
25
- // INSERT INTO [schema.]table
26
- const insertMatch = trimmed.match(
27
- /^\s*INSERT\s+INTO\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?/i
28
- );
29
- if (insertMatch) {
30
- return {
31
- operation: 'upsert',
32
- tableName: insertMatch[2]!,
33
- };
222
+ const statement = extractMainStatement(sql).trimStart();
223
+ const lower = statement.toLowerCase();
224
+
225
+ if (lower.startsWith('insert')) {
226
+ const tableName = parseTargetTable(
227
+ statement.replace(/^insert\s+into\s+/i, '')
228
+ );
229
+ if (!tableName) return null;
230
+ return { operation: 'upsert', tableName };
34
231
  }
35
232
 
36
- // UPDATE [schema.]table
37
- const updateMatch = trimmed.match(
38
- /^\s*UPDATE\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?/i
39
- );
40
- if (updateMatch) {
41
- return {
42
- operation: 'upsert',
43
- tableName: updateMatch[2]!,
44
- };
233
+ if (lower.startsWith('update')) {
234
+ const tableName = parseTargetTable(statement.replace(/^update\s+/i, ''));
235
+ if (!tableName) return null;
236
+ return { operation: 'upsert', tableName };
45
237
  }
46
238
 
47
- // DELETE FROM [schema.]table
48
- const deleteMatch = trimmed.match(
49
- /^\s*DELETE\s+FROM\s+(?:["']?(\w+)["']?\.)?["']?(\w+)["']?/i
50
- );
51
- if (deleteMatch) {
52
- return {
53
- operation: 'delete',
54
- tableName: deleteMatch[2]!,
55
- };
239
+ if (lower.startsWith('delete')) {
240
+ const tableName = parseTargetTable(
241
+ statement.replace(/^delete\s+from\s+/i, '')
242
+ );
243
+ if (!tableName) return null;
244
+ return { operation: 'delete', tableName };
56
245
  }
57
246
 
58
247
  return null;
@@ -66,6 +255,15 @@ export function hasReturningClause(sql: string): boolean {
66
255
  return /\bRETURNING\b/i.test(sql);
67
256
  }
68
257
 
258
+ /**
259
+ * Check if SQL has a wildcard RETURNING clause (RETURNING * or alias.*).
260
+ */
261
+ export function hasReturningWildcard(sql: string): boolean {
262
+ const match = sql.match(/\bRETURNING\b([\s\S]*)$/i);
263
+ if (!match) return false;
264
+ return /(^|,)\s*(?:[A-Za-z_][A-Za-z0-9_$]*\.)?\*/i.test(match[1]);
265
+ }
266
+
69
267
  /**
70
268
  * Append RETURNING * to a mutation query if not already present.
71
269
  *