diff --git a/.changeset/funny-pianos-allow.md b/.changeset/funny-pianos-allow.md
deleted file mode 100644
index 3df90e3bc..000000000
--- a/.changeset/funny-pianos-allow.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@powersync/service-core': patch
----
-
-Fix "operation exceeded time limit" error
diff --git a/.changeset/green-books-shout.md b/.changeset/green-books-shout.md
deleted file mode 100644
index 1b2ef8139..000000000
--- a/.changeset/green-books-shout.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@powersync/service-core': patch
----
-
-Fix storageStats error in metrics endpoint when collections don't exist.
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3705cd600..e8a6f47ad 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,7 +10,7 @@ on:
jobs:
test-service-container-build:
- name: Build and Test Powersync Service
+ name: Build and Test PowerSync Service
runs-on: ubuntu-latest
steps:
- name: Checkout
diff --git a/DEVELOP.md b/DEVELOP.md
index 23eb51d4d..3b88419e6 100644
--- a/DEVELOP.md
+++ b/DEVELOP.md
@@ -18,7 +18,7 @@ pnpm build
## Dependent Services
-The PowerSync service requires Postgres and MongoDB server connections. These configuration details can be specified in a `powersync.yaml` (or JSON) configuration file.
+The PowerSync Service requires Postgres and MongoDB server connections. These configuration details can be specified in a `powersync.yaml` (or JSON) configuration file.
See the [self-hosting demo](https://github.com/powersync-ja/self-host-demo) for demos of starting these services.
@@ -36,7 +36,7 @@ One method to obtain access is to add the following to `/etc/hosts` (on Unix-lik
127.0.0.1 mongo
```
-This will start all the services defined in the Self hosting demo except for the PowerSync service - which will be started from this repository.
+This will start all the services defined in the self-hosting demo except for the PowerSync Service - which will be started from this repository.
## Local Configuration
diff --git a/README.md b/README.md
index 8b994b142..1e9ac4fbb 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
-_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres or MongoDB on the server-side (MySQL coming soon)._
+_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side._
# PowerSync Service
diff --git a/modules/module-postgres/src/types/types.ts b/modules/module-postgres/src/types/types.ts
index 9b5be2d2b..858a30f2a 100644
--- a/modules/module-postgres/src/types/types.ts
+++ b/modules/module-postgres/src/types/types.ts
@@ -142,11 +142,10 @@ export function validatePort(port: string | number): number {
if (typeof port == 'string') {
port = parseInt(port);
}
- if (port >= 1024 && port <= 49151) {
- return port;
- } else {
+ if (port < 1024) {
throw new Error(`Port ${port} not supported`);
- }
+ }
+ return port;
}
/**
diff --git a/packages/service-core/CHANGELOG.md b/packages/service-core/CHANGELOG.md
index e76e019d4..4a718373f 100644
--- a/packages/service-core/CHANGELOG.md
+++ b/packages/service-core/CHANGELOG.md
@@ -1,5 +1,20 @@
# @powersync/service-core
+## 0.8.7
+
+### Patch Changes
+
+- 6b72e6c: Improved Postgres connection port restrictions. Connections are now supported on ports >= 1024.
+
+## 0.8.6
+
+### Patch Changes
+
+- 2d3bb6a: Fix "operation exceeded time limit" error
+- 17a6db0: Fix storageStats error in metrics endpoint when collections don't exist.
+- Updated dependencies [0f90b02]
+ - @powersync/service-sync-rules@0.20.0
+
## 0.8.5
### Patch Changes
diff --git a/packages/service-core/package.json b/packages/service-core/package.json
index b2fe5b340..865974eab 100644
--- a/packages/service-core/package.json
+++ b/packages/service-core/package.json
@@ -5,7 +5,7 @@
"publishConfig": {
"access": "public"
},
- "version": "0.8.5",
+ "version": "0.8.7",
"main": "dist/index.js",
"license": "FSL-1.1-Apache-2.0",
"type": "module",
@@ -13,7 +13,7 @@
"build": "tsc -b",
"build:tests": "tsc -b test/tsconfig.json",
"test": "vitest",
- "clean": "rm -rf ./lib && tsc -b --clean"
+ "clean": "rm -rf ./dist && tsc -b --clean"
},
"dependencies": {
"@js-sdsl/ordered-set": "^4.4.2",
diff --git a/packages/sync-rules/CHANGELOG.md b/packages/sync-rules/CHANGELOG.md
index 099bf11db..d74892def 100644
--- a/packages/sync-rules/CHANGELOG.md
+++ b/packages/sync-rules/CHANGELOG.md
@@ -1,5 +1,11 @@
# @powersync/service-sync-rules
+## 0.20.0
+
+### Minor Changes
+
+- 0f90b02: Support substring and json_keys functions in sync rules
+
## 0.19.0
### Minor Changes
diff --git a/packages/sync-rules/package.json b/packages/sync-rules/package.json
index 727188bb6..085f36f7b 100644
--- a/packages/sync-rules/package.json
+++ b/packages/sync-rules/package.json
@@ -1,7 +1,7 @@
{
"name": "@powersync/service-sync-rules",
"repository": "https://github.com/powersync-ja/powersync-service",
- "version": "0.19.0",
+ "version": "0.20.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "FSL-1.1-Apache-2.0",
diff --git a/packages/sync-rules/src/sql_functions.ts b/packages/sync-rules/src/sql_functions.ts
index ce419d49a..fd6f71b91 100644
--- a/packages/sync-rules/src/sql_functions.ts
+++ b/packages/sync-rules/src/sql_functions.ts
@@ -82,6 +82,61 @@ const lower: DocumentedSqlFunction = {
detail: 'Convert text to lower case'
};
+const substring: DocumentedSqlFunction = {
+ debugName: 'substring',
+ call(value: SqliteValue, start: SqliteValue, length?: SqliteValue) {
+ const text = castAsText(value);
+ if (text == null) {
+ return null;
+ }
+ const startIndex = cast(start, 'integer') as bigint | null;
+ if (startIndex == null) {
+ return null;
+ }
+ if (length === null) {
+ // Different from undefined in this case, to match SQLite behavior
+ return null;
+ }
+ const castLength = cast(length ?? null, 'integer') as bigint | null;
+ let realLength: number;
+ if (castLength == null) {
+ // undefined (not specified)
+ realLength = text.length + 1; // +1 to account for the start = 0 special case
+ } else {
+ realLength = Number(castLength);
+ }
+
+ let realStart = 0;
+ if (startIndex < 0n) {
+ realStart = Math.max(0, text.length + Number(startIndex));
+ } else if (startIndex == 0n) {
+ // Weird special case
+ realStart = 0;
+ realLength -= 1;
+ } else {
+ realStart = Number(startIndex) - 1;
+ }
+
+ if (realLength < 0) {
+ // Negative length means we return that many characters _before_
+ // the start index.
+ return text.substring(realStart + realLength, realStart);
+ }
+
+ return text.substring(realStart, realStart + realLength);
+ },
+ parameters: [
+ { name: 'value', type: ExpressionType.TEXT, optional: false },
+ { name: 'start', type: ExpressionType.INTEGER, optional: false },
+ { name: 'length', type: ExpressionType.INTEGER, optional: true }
+ ],
+ getReturnType(args) {
+ return ExpressionType.TEXT;
+ },
+ detail: 'Compute a substring',
+ documentation: 'The start index starts at 1. If no length is specified, the remainder of the string is returned.'
+};
+
const hex: DocumentedSqlFunction = {
debugName: 'hex',
call(value: SqliteValue) {
@@ -235,6 +290,32 @@ const json_valid: DocumentedSqlFunction = {
documentation: 'Returns 1 if valid, 0 if invalid'
};
+const json_keys: DocumentedSqlFunction = {
+ debugName: 'json_keys',
+ call(json: SqliteValue) {
+ const jsonString = castAsText(json);
+ if (jsonString == null) {
+ return null;
+ }
+
+ const jsonParsed = JSONBig.parse(jsonString);
+ if (typeof jsonParsed != 'object') {
+ throw new Error(`Cannot call json_keys on a scalar`);
+ } else if (Array.isArray(jsonParsed)) {
+ throw new Error(`Cannot call json_keys on an array`);
+ }
+ const keys = Object.keys(jsonParsed as {});
+ // Keys are always strings, safe to use plain JSON.
+ return JSON.stringify(keys);
+ },
+ parameters: [{ name: 'json', type: ExpressionType.ANY, optional: false }],
+ getReturnType(args) {
+ // TODO: proper nullable types
+ return ExpressionType.TEXT;
+ },
+ detail: 'Returns the keys of a JSON object as a JSON array'
+};
+
const unixepoch: DocumentedSqlFunction = {
debugName: 'unixepoch',
call(value?: SqliteValue, specifier?: SqliteValue, specifier2?: SqliteValue) {
@@ -401,6 +482,7 @@ const st_y: DocumentedSqlFunction = {
export const SQL_FUNCTIONS_NAMED = {
upper,
lower,
+ substring,
hex,
length,
base64,
@@ -409,6 +491,7 @@ export const SQL_FUNCTIONS_NAMED = {
json_extract,
json_array_length,
json_valid,
+ json_keys,
unixepoch,
datetime,
st_asgeojson,
diff --git a/packages/sync-rules/test/src/sql_functions.test.ts b/packages/sync-rules/test/src/sql_functions.test.ts
index 525133c3b..1a155d6af 100644
--- a/packages/sync-rules/test/src/sql_functions.test.ts
+++ b/packages/sync-rules/test/src/sql_functions.test.ts
@@ -44,6 +44,30 @@ describe('SQL functions', () => {
expect(fn.json_array_length(`{"a":[1,2,3,4]}`)).toEqual(0n);
});
+ test('json_keys', () => {
+ expect(fn.json_keys(`{"a": 1, "b": "2", "0": "test", "c": {"d": "e"}}`)).toEqual(`["0","a","b","c"]`);
+ expect(fn.json_keys(`{}`)).toEqual(`[]`);
+ expect(fn.json_keys(null)).toEqual(null);
+ expect(fn.json_keys()).toEqual(null);
+ expect(() => fn.json_keys(`{"a": 1, "a": 2}`)).toThrow();
+ expect(() => fn.json_keys(`[1,2,3]`)).toThrow();
+ expect(() => fn.json_keys(3)).toThrow();
+ });
+
+ test('json_valid', () => {
+ expect(fn.json_valid(`{"a": 1, "b": "2", "0": "test", "c": {"d": "e"}}`)).toEqual(1n);
+ expect(fn.json_valid(`{}`)).toEqual(1n);
+ expect(fn.json_valid(null)).toEqual(0n);
+ expect(fn.json_valid()).toEqual(0n);
+ expect(fn.json_valid(`{"a": 1, "a": 2}`)).toEqual(0n);
+ expect(fn.json_valid(`[1,2,3]`)).toEqual(1n);
+ expect(fn.json_valid(3)).toEqual(1n);
+ expect(fn.json_valid('test')).toEqual(0n);
+ expect(fn.json_valid('"test"')).toEqual(1n);
+ expect(fn.json_valid('true')).toEqual(1n);
+ expect(fn.json_valid('TRUE')).toEqual(0n);
+ });
+
test('typeof', () => {
expect(fn.typeof(null)).toEqual('null');
expect(fn.typeof('test')).toEqual('text');
@@ -101,6 +125,22 @@ describe('SQL functions', () => {
expect(fn.lower(Uint8Array.of(0x61, 0x62, 0x43))).toEqual('abc');
});
+ test('substring', () => {
+ expect(fn.substring(null)).toEqual(null);
+ expect(fn.substring('abc')).toEqual(null);
+ expect(fn.substring('abcde', 2, 3)).toEqual('bcd');
+ expect(fn.substring('abcde', 2)).toEqual('bcde');
+ expect(fn.substring('abcde', 2, null)).toEqual(null);
+ expect(fn.substring('abcde', 0, 1)).toEqual('');
+ expect(fn.substring('abcde', 0, 2)).toEqual('a');
+ expect(fn.substring('abcde', 1, 2)).toEqual('ab');
+ expect(fn.substring('abcde', -2)).toEqual('de');
+ expect(fn.substring('abcde', -2, 1)).toEqual('d');
+ expect(fn.substring('abcde', 6, -5)).toEqual('abcde');
+ expect(fn.substring('abcde', 5, -2)).toEqual('cd');
+ expect(fn.substring('2023-06-28 14:12:00.999Z', 1, 10)).toEqual('2023-06-28');
+ });
+
test('cast', () => {
expect(cast(null, 'text')).toEqual(null);
expect(cast(null, 'integer')).toEqual(null);
diff --git a/service/CHANGELOG.md b/service/CHANGELOG.md
index 6c12e7529..fc767a8cf 100644
--- a/service/CHANGELOG.md
+++ b/service/CHANGELOG.md
@@ -1,5 +1,22 @@
# @powersync/service-image
+## 0.5.7
+
+### Patch Changes
+
+- Updated dependencies [6b72e6c]
+ - @powersync/service-core@0.8.7
+
+## 0.5.6
+
+### Patch Changes
+
+- Updated dependencies [2d3bb6a]
+- Updated dependencies [17a6db0]
+- Updated dependencies [0f90b02]
+ - @powersync/service-core@0.8.6
+ - @powersync/service-sync-rules@0.20.0
+
## 0.5.5
### Patch Changes
diff --git a/service/README.md b/service/README.md
index 810e45a54..668d4575d 100644
--- a/service/README.md
+++ b/service/README.md
@@ -2,7 +2,7 @@
-_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres or MongoDB on the server-side (MySQL coming soon)._
+_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side._
# Quick reference
diff --git a/service/package.json b/service/package.json
index 95e1f5296..33caf860f 100644
--- a/service/package.json
+++ b/service/package.json
@@ -1,6 +1,6 @@
{
"name": "@powersync/service-image",
- "version": "0.5.5",
+ "version": "0.5.7",
"private": true,
"license": "FSL-1.1-Apache-2.0",
"type": "module",
diff --git a/test-client/CHANGELOG.md b/test-client/CHANGELOG.md
index 9c90f775c..21411c998 100644
--- a/test-client/CHANGELOG.md
+++ b/test-client/CHANGELOG.md
@@ -1,5 +1,20 @@
# test-client
+## 0.1.9
+
+### Patch Changes
+
+- Updated dependencies [6b72e6c]
+ - @powersync/service-core@0.8.7
+
+## 0.1.8
+
+### Patch Changes
+
+- Updated dependencies [2d3bb6a]
+- Updated dependencies [17a6db0]
+ - @powersync/service-core@0.8.6
+
## 0.1.7
### Patch Changes
diff --git a/test-client/package.json b/test-client/package.json
index 5e311c708..e4eb17e92 100644
--- a/test-client/package.json
+++ b/test-client/package.json
@@ -2,7 +2,7 @@
"name": "test-client",
"repository": "https://github.com/powersync-ja/powersync-service",
"private": true,
- "version": "0.1.7",
+ "version": "0.1.9",
"main": "dist/index.js",
"bin": "dist/bin.js",
"license": "Apache-2.0",