├── .nvmrc ├── service ├── .probes │ └── .gitkeep ├── local-dev │ ├── sync-rules-template.yaml │ └── powersync-template.yaml ├── src │ ├── util │ │ ├── version.ts │ │ └── modules.ts │ └── runners │ │ ├── server.ts │ │ └── unified-runner.ts ├── tsconfig.json └── package.json ├── packages ├── schema │ ├── .gitignore │ ├── src │ │ └── index.ts │ ├── README.md │ ├── tsconfig.json │ ├── CHANGELOG.md │ └── package.json ├── service-core │ ├── .probes │ │ └── .gitkeep │ ├── src │ │ ├── system │ │ │ └── system-index.ts │ │ ├── migrations │ │ │ ├── migrations-index.ts │ │ │ └── ensure-automatic-migrations.ts │ │ ├── storage │ │ │ ├── ReplicationLock.ts │ │ │ ├── ReplicationEventPayload.ts │ │ │ ├── storage-index.ts │ │ │ ├── SourceEntity.ts │ │ │ ├── PersistedSyncRulesContent.ts │ │ │ └── StorageProvider.ts │ │ ├── util │ │ │ ├── version.ts │ │ │ ├── alerting.ts │ │ │ ├── lsn.ts │ │ │ ├── config │ │ │ │ ├── defaults.ts │ │ │ │ ├── sync-rules │ │ │ │ │ ├── sync-collector.ts │ │ │ │ │ ├── impl │ │ │ │ │ │ ├── inline-sync-rules-collector.ts │ │ │ │ │ │ ├── base64-sync-rules-collector.ts │ │ │ │ │ │ └── filesystem-sync-rules-collector.ts │ │ │ │ │ └── sync-rules-provider.ts │ │ │ │ └── collectors │ │ │ │ │ └── impl │ │ │ │ │ ├── base64-config-collector.ts │ │ │ │ │ └── fallback-config-collector.ts │ │ │ ├── env.ts │ │ │ ├── config.ts │ │ │ └── util-index.ts │ │ ├── modules │ │ │ ├── modules-index.ts │ │ │ └── AbstractModule.ts │ │ ├── streams │ │ │ └── streams-index.ts │ │ ├── api │ │ │ ├── api-index.ts │ │ │ └── schema.ts │ │ ├── replication │ │ │ ├── ErrorRateLimiter.ts │ │ │ ├── replication-index.ts │ │ │ └── RelationCache.ts │ │ ├── sync │ │ │ ├── sync-index.ts │ │ │ └── SyncContext.ts │ │ ├── routes │ │ │ ├── endpoints │ │ │ │ └── route-endpoints-index.ts │ │ │ ├── routes-index.ts │ │ │ └── router-socket.ts │ │ ├── metrics │ │ │ ├── metrics-index.ts │ │ │ └── metrics-interfaces.ts │ │ ├── entry │ │ │ ├── entry-index.ts │ │ │ └── commands │ │ │ │ ├── start-action.ts │ │ │ │ ├── teardown-action.ts │ │ │ │ └── config-command.ts │ │ └── auth │ │ │ ├── JwtPayload.ts │ │ │ ├── auth-index.ts │ │ │ ├── StaticKeyCollector.ts │ │ │ ├── KeyCollector.ts │ │ │ ├── CompoundKeyCollector.ts │ │ │ └── StaticSupabaseKeyCollector.ts │ ├── README.md │ ├── test │ │ ├── src │ │ │ ├── env.ts │ │ │ ├── setup.ts │ │ │ └── sync │ │ │ │ └── util.test.ts │ │ └── tsconfig.json │ ├── vitest.config.ts │ └── tsconfig.json ├── sync-rules │ ├── .gitignore │ ├── src │ │ ├── IdSequence.ts │ │ ├── SourceTableInterface.ts │ │ ├── wkx.d.ts │ │ ├── streams │ │ │ └── utils.ts │ │ ├── schema-generators │ │ │ ├── schema-generators.ts │ │ │ └── generators.ts │ │ ├── index.ts │ │ └── TableQuerySchema.ts │ ├── vitest.config.ts │ ├── test │ │ ├── matchers.d.ts │ │ ├── tsconfig.json │ │ └── matchers.ts │ ├── tsconfig.json │ ├── scripts │ │ └── compile-schema.js │ └── package.json ├── service-errors │ ├── README.md │ ├── src │ │ ├── index.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── test │ │ ├── tsconfig.json │ │ └── src │ │ │ └── codes.test.ts │ └── package.json ├── rsocket-router │ ├── README.md │ ├── src │ │ ├── index.ts │ │ ├── router │ │ │ └── SocketRouterListener.ts │ │ └── utils │ │ │ └── BaseObserver.ts │ ├── tsconfig.json │ ├── tests │ │ ├── tsconfig.json │ │ └── src │ │ │ └── utils │ │ │ └── mock-responder.ts │ └── package.json ├── service-core-tests │ ├── src │ │ ├── tests │ │ │ ├── util.ts │ │ │ └── tests-index.ts │ │ ├── test-utils │ │ │ ├── test-utils-index.ts │ │ │ └── stream_utils.ts │ │ └── index.ts │ ├── README.md │ ├── tsconfig.json │ └── package.json ├── jsonbig │ ├── src │ │ ├── index.ts │ │ ├── json_container.ts │ │ └── json.ts │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── package.json │ └── README.md ├── jpgwire │ ├── src │ │ ├── index.ts │ │ ├── metrics.ts │ │ ├── certs.ts │ │ ├── pgwire.ts │ │ └── pgwire_node.js │ ├── README.md │ ├── test │ │ └── tsconfig.json │ ├── tsconfig.json │ ├── package.json │ └── ca │ │ └── README.md └── types │ ├── tsconfig.json │ ├── src │ └── index.ts │ └── package.json ├── modules ├── module-mssql │ ├── .npmignore │ ├── src │ │ ├── index.ts │ │ ├── common │ │ │ └── MSSQLSourceTableCache.ts │ │ └── replication │ │ │ ├── MSSQLConnectionManagerFactory.ts │ │ │ └── MSSQLErrorRateLimiter.ts │ ├── README.md │ ├── dev │ │ └── .env.template │ ├── vitest.config.ts │ ├── test │ │ ├── src │ │ │ ├── setup.ts │ │ │ └── env.ts │ │ └── tsconfig.json │ ├── tsconfig.json │ └── CHANGELOG.md ├── module-core │ ├── src │ │ ├── index.ts │ │ └── types │ │ │ └── types.ts │ ├── README.md │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── test │ │ └── tsconfig.json │ └── package.json ├── module-mysql │ ├── src │ │ ├── index.ts │ │ ├── common │ │ │ └── common-index.ts │ │ ├── types │ │ │ └── node-sql-parser-extended-types.ts │ │ └── replication │ │ │ └── MySQLConnectionManagerFactory.ts │ ├── dev │ │ ├── .env.template │ │ ├── docker │ │ │ └── mysql │ │ │ │ ├── init-scripts │ │ │ │ ├── my.cnf │ │ │ │ └── mysql_57.sql │ │ │ │ ├── docker-compose.yaml │ │ │ │ └── docker-compose-57.yaml │ │ ├── config │ │ │ └── sync_rules.yaml │ │ └── README.md │ ├── README.md │ ├── vitest.config.ts │ ├── test │ │ ├── src │ │ │ ├── setup.ts │ │ │ ├── env.ts │ │ │ ├── mysql-utils.test.ts │ │ │ └── parser-utils.test.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── module-postgres │ ├── src │ │ ├── index.ts │ │ ├── replication │ │ │ ├── replication-index.ts │ │ │ └── ConnectionManagerFactory.ts │ │ └── utils │ │ │ ├── application-name.ts │ │ │ ├── postgres_version.ts │ │ │ └── populate_test_data.ts │ ├── README.md │ ├── test │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ └── schema_changes.test.ts.snap │ │ │ ├── setup.ts │ │ │ └── env.ts │ │ └── tsconfig.json │ ├── vitest.config.ts │ └── tsconfig.json ├── module-mongodb │ ├── README.md │ ├── src │ │ ├── utils.ts │ │ ├── index.ts │ │ └── replication │ │ │ ├── replication-index.ts │ │ │ └── ConnectionManagerFactory.ts │ ├── vitest.config.ts │ ├── test │ │ ├── src │ │ │ ├── setup.ts │ │ │ └── env.ts │ │ └── tsconfig.json │ └── tsconfig.json ├── module-mongodb-storage │ ├── src │ │ ├── utils │ │ │ └── utils-index.ts │ │ ├── migrations │ │ │ └── db │ │ │ │ └── migrations │ │ │ │ ├── 1688556755264-initial-sync-rules.ts │ │ │ │ ├── 1752661449910-connection-reporting.ts │ │ │ │ ├── 1741697235857-bucket-state-index.ts │ │ │ │ ├── 1760433882550-bucket-state-index2.ts │ │ │ │ ├── 1711543888062-write-checkpoint-index.ts │ │ │ │ ├── 1727099539247-custom-write-checkpoint-index.ts │ │ │ │ └── 1684951997326-init.ts │ │ ├── index.ts │ │ ├── storage │ │ │ ├── implementation │ │ │ │ ├── MongoPersistedSyncRules.ts │ │ │ │ └── MongoIdSequence.ts │ │ │ └── storage-index.ts │ │ ├── types │ │ │ └── types.ts │ │ └── module │ │ │ └── MongoStorageModule.ts │ ├── README.md │ ├── test │ │ ├── src │ │ │ ├── env.ts │ │ │ ├── setup.ts │ │ │ ├── util.ts │ │ │ ├── migrations.test.ts │ │ │ └── __snapshots__ │ │ │ │ └── storage.test.ts.snap │ │ └── tsconfig.json │ ├── vitest.config.ts │ └── tsconfig.json └── module-postgres-storage │ ├── src │ ├── utils │ │ ├── utils-index.ts │ │ ├── application-name.ts │ │ ├── ts-codec.ts │ │ ├── bson.ts │ │ ├── bucket-data.ts │ │ └── db.ts │ ├── storage │ │ └── storage-index.ts │ ├── types │ │ └── models │ │ │ ├── Instance.ts │ │ │ ├── models-index.ts │ │ │ ├── BucketParameters.ts │ │ │ ├── ActiveCheckpoint.ts │ │ │ ├── Migration.ts │ │ │ ├── SdkReporting.ts │ │ │ ├── WriteCheckpoint.ts │ │ │ ├── ActiveCheckpointNotification.ts │ │ │ ├── CurrentData.ts │ │ │ ├── BucketData.ts │ │ │ └── SourceTable.ts │ ├── index.ts │ ├── migrations │ │ └── migration-utils.ts │ └── module │ │ └── PostgresStorageModule.ts │ ├── test │ ├── src │ │ ├── storage_compacting.test.ts │ │ ├── env.ts │ │ ├── __snapshots__ │ │ │ └── storage.test.ts.snap │ │ ├── setup.ts │ │ ├── storage_sync.test.ts │ │ └── util.ts │ └── tsconfig.json │ ├── vitest.config.ts │ └── tsconfig.json ├── .envrc ├── vitest.workspace.ts ├── libs ├── lib-services │ ├── src │ │ ├── ip │ │ │ └── ip-index.ts │ │ ├── system │ │ │ └── system-index.ts │ │ ├── errors │ │ │ └── errors-index.ts │ │ ├── codec │ │ │ └── codec-index.ts │ │ ├── logger │ │ │ ├── logger-index.ts │ │ │ └── Logger.ts │ │ ├── alerts │ │ │ ├── alerts-index.ts │ │ │ ├── no-op-reporter.ts │ │ │ └── definitions.ts │ │ ├── locks │ │ │ ├── locks-index.ts │ │ │ └── LockManager.ts │ │ ├── utils │ │ │ ├── utils-index.ts │ │ │ └── BaseObserver.ts │ │ ├── router │ │ │ ├── router-index.ts │ │ │ └── endpoint.ts │ │ ├── migrations │ │ │ ├── migrations-index.ts │ │ │ ├── migration-definitions.ts │ │ │ └── MigrationManager.ts │ │ ├── signals │ │ │ ├── signals-index.ts │ │ │ └── probes │ │ │ │ ├── probes.ts │ │ │ │ └── memory-probes.ts │ │ ├── schema │ │ │ ├── schema-index.ts │ │ │ ├── definitions.ts │ │ │ ├── json-schema │ │ │ │ └── keywords.ts │ │ │ └── validators │ │ │ │ └── ts-codec-validator.ts │ │ └── index.ts │ ├── README.md │ ├── tsconfig.json │ ├── test │ │ ├── tsconfig.json │ │ ├── src │ │ │ ├── __snapshots__ │ │ │ │ ├── errors.test.ts.snap │ │ │ │ └── ts-codec-validation.test.ts.snap │ │ │ ├── schema │ │ │ │ └── __snapshots__ │ │ │ │ │ └── ts-codec-validation.test.ts.snap │ │ │ └── fixtures │ │ │ │ └── schema.ts │ │ └── __snapshots__ │ │ │ └── errors.test.ts.snap │ └── package.json ├── lib-mongodb │ ├── src │ │ ├── locks │ │ │ └── locks-index.ts │ │ ├── db │ │ │ └── db-index.ts │ │ ├── index.ts │ │ └── types │ │ │ └── mongodb-connection-string-url.d.ts │ ├── vitest.config.ts │ ├── README.md │ ├── tsconfig.json │ ├── test │ │ └── tsconfig.json │ └── package.json └── lib-postgres │ ├── src │ ├── locks │ │ └── locks-index.ts │ ├── utils │ │ ├── utils-index.ts │ │ └── identifier-utils.ts │ ├── db │ │ ├── db-index.ts │ │ └── connection │ │ │ └── WrappedConnection.ts │ └── index.ts │ ├── vitest.config.ts │ ├── README.md │ ├── tsconfig.json │ ├── test │ ├── tsconfig.json │ └── src │ │ └── config.test.ts │ └── package.json ├── .prettierignore ├── test-client ├── src │ ├── index.ts │ └── load-testing │ │ └── load-test.ts ├── tsconfig.json └── package.json ├── .changeset ├── ten-walls-cough.md ├── afraid-weeks-matter.md ├── nasty-snakes-double.md ├── config.json ├── short-pumpkins-retire.md └── README.md ├── pnpm-workspace.yaml ├── docs └── README.md ├── .env.template ├── .gitignore ├── .prettierrc ├── tsconfig.base.json ├── CODE_OF_CONDUCT.md └── tsconfig.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.21.1 2 | -------------------------------------------------------------------------------- /service/.probes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/schema/.gitignore: -------------------------------------------------------------------------------- 1 | json-schema/ -------------------------------------------------------------------------------- /packages/service-core/.probes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/sync-rules/.gitignore: -------------------------------------------------------------------------------- 1 | schema/ -------------------------------------------------------------------------------- /modules/module-mssql/.npmignore: -------------------------------------------------------------------------------- 1 | dev 2 | ci -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | layout node 2 | use node 3 | [ -f .env ] && dotenv -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default ['modules/*', 'packages/*']; 2 | -------------------------------------------------------------------------------- /libs/lib-services/src/ip/ip-index.ts: -------------------------------------------------------------------------------- 1 | export * from './lookup.js'; 2 | -------------------------------------------------------------------------------- /modules/module-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CoreModule.js'; 2 | -------------------------------------------------------------------------------- /modules/module-core/src/types/types.ts: -------------------------------------------------------------------------------- 1 | // No types for this module yet 2 | -------------------------------------------------------------------------------- /modules/module-mssql/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module/MSSQLModule.js'; 2 | -------------------------------------------------------------------------------- /modules/module-mysql/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module/MySQLModule.js'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/.git 2 | **/node_modules 3 | dist 4 | lib 5 | pnpm-lock.yaml 6 | -------------------------------------------------------------------------------- /libs/lib-mongodb/src/locks/locks-index.ts: -------------------------------------------------------------------------------- 1 | export * from './MongoLockManager.js'; 2 | -------------------------------------------------------------------------------- /libs/lib-postgres/src/locks/locks-index.ts: -------------------------------------------------------------------------------- 1 | export * from './PostgresLockManager.js'; 2 | -------------------------------------------------------------------------------- /libs/lib-services/src/system/system-index.ts: -------------------------------------------------------------------------------- 1 | export * from './LifeCycledSystem.js'; 2 | -------------------------------------------------------------------------------- /modules/module-postgres/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module/PostgresModule.js'; 2 | -------------------------------------------------------------------------------- /libs/lib-services/src/errors/errors-index.ts: -------------------------------------------------------------------------------- 1 | export * from '@powersync/service-errors'; 2 | -------------------------------------------------------------------------------- /packages/service-core/src/system/system-index.ts: -------------------------------------------------------------------------------- 1 | export * from './ServiceContext.js'; 2 | -------------------------------------------------------------------------------- /libs/lib-mongodb/src/db/db-index.ts: -------------------------------------------------------------------------------- 1 | export * from './mongo.js'; 2 | export * from './errors.js'; 3 | -------------------------------------------------------------------------------- /modules/module-mssql/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync MSSQL Module 2 | 3 | MSSQL replication module for PowerSync 4 | -------------------------------------------------------------------------------- /modules/module-mysql/dev/.env.template: -------------------------------------------------------------------------------- 1 | PS_MONGO_URI=mongodb://mongo:27017/powersync_demo 2 | PS_PORT=8080 -------------------------------------------------------------------------------- /libs/lib-services/src/codec/codec-index.ts: -------------------------------------------------------------------------------- 1 | export * from './codecs.js'; 2 | export * from './parsers.js'; 3 | -------------------------------------------------------------------------------- /libs/lib-services/src/logger/logger-index.ts: -------------------------------------------------------------------------------- 1 | export * from './Logger.js'; 2 | export { Logger } from 'winston'; 3 | -------------------------------------------------------------------------------- /libs/lib-services/src/alerts/alerts-index.ts: -------------------------------------------------------------------------------- 1 | export * from './definitions.js'; 2 | export * from './no-op-reporter.js'; 3 | -------------------------------------------------------------------------------- /modules/module-mongodb/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Module MongoDB 2 | 3 | MongoDB replication module for PowerSync 4 | -------------------------------------------------------------------------------- /packages/service-errors/README.md: -------------------------------------------------------------------------------- 1 | # @powersync/service-errors 2 | 3 | Defines error codes used across the service. 4 | -------------------------------------------------------------------------------- /test-client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client.js'; 2 | export * from './ndjson.js'; 3 | export * from './util.js'; 4 | -------------------------------------------------------------------------------- /libs/lib-mongodb/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /libs/lib-postgres/src/utils/utils-index.ts: -------------------------------------------------------------------------------- 1 | export * from './identifier-utils.js'; 2 | export * from './pgwire_utils.js'; 3 | -------------------------------------------------------------------------------- /libs/lib-postgres/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /libs/lib-services/src/locks/locks-index.ts: -------------------------------------------------------------------------------- 1 | export * from './AbstractLockManager.js'; 2 | export * from './LockManager.js'; 3 | -------------------------------------------------------------------------------- /modules/module-postgres/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Module Postgres 2 | 3 | Postgres replication module for PowerSync 4 | -------------------------------------------------------------------------------- /libs/lib-mongodb/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service MongoDB 2 | 3 | Library for common MongoDB logic used in the PowerSync service. 4 | -------------------------------------------------------------------------------- /libs/lib-services/README.md: -------------------------------------------------------------------------------- 1 | # Service Library 2 | 3 | A library containing base definitions for interacting with Micro services. 4 | -------------------------------------------------------------------------------- /libs/lib-services/src/utils/utils-index.ts: -------------------------------------------------------------------------------- 1 | export * from './BaseObserver.js'; 2 | export * from './environment-variables.js'; 3 | -------------------------------------------------------------------------------- /modules/module-mysql/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync MySQL Module 2 | 3 | This is a module which provides MySQL replication to PowerSync. 4 | -------------------------------------------------------------------------------- /packages/service-errors/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors.js'; 2 | export * from './codes.js'; 3 | export * from './utils.js'; 4 | -------------------------------------------------------------------------------- /libs/lib-postgres/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Postgres 2 | 3 | Library for common Postgres logic used in the PowerSync service. 4 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/utils/utils-index.ts: -------------------------------------------------------------------------------- 1 | export * as test_utils from './test-utils.js'; 2 | export * from './util.js'; 3 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Module MongoDB Storage 2 | 3 | MongoDB bucket storage module for PowerSync 4 | -------------------------------------------------------------------------------- /packages/service-core/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Core 2 | 3 | This package contains core logic used by the PowerSync backend services. 4 | -------------------------------------------------------------------------------- /modules/module-core/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Module Core 2 | 3 | Module which registers and configures basic core functionality for PowerSync services. 4 | -------------------------------------------------------------------------------- /packages/service-core/src/migrations/migrations-index.ts: -------------------------------------------------------------------------------- 1 | export * from './ensure-automatic-migrations.js'; 2 | export * from './PowerSyncMigrationManager.js'; 3 | -------------------------------------------------------------------------------- /packages/service-core/src/storage/ReplicationLock.ts: -------------------------------------------------------------------------------- 1 | export interface ReplicationLock { 2 | sync_rules_id: number; 3 | 4 | release(): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /packages/service-core/src/util/version.ts: -------------------------------------------------------------------------------- 1 | import pkg from '../../package.json' with { type: 'json' }; 2 | 3 | export const POWERSYNC_VERSION = pkg.version; 4 | -------------------------------------------------------------------------------- /packages/sync-rules/src/IdSequence.ts: -------------------------------------------------------------------------------- 1 | export class IdSequence { 2 | private count = 0; 3 | 4 | nextId() { 5 | return `${++this.count}`; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/lib-services/src/router/router-index.ts: -------------------------------------------------------------------------------- 1 | export * from './router-definitions.js'; 2 | export * from './router-response.js'; 3 | export * from './endpoint.js'; 4 | -------------------------------------------------------------------------------- /packages/rsocket-router/README.md: -------------------------------------------------------------------------------- 1 | # RSocket Micro Router 2 | 3 | This package wraps the RSocket library to a router format compatible with endpoint definitions. 4 | -------------------------------------------------------------------------------- /packages/schema/src/index.ts: -------------------------------------------------------------------------------- 1 | // This project does not yet include TypeScript source, but it does use TypeScript to depend on the build of @powersync/service-types. 2 | -------------------------------------------------------------------------------- /packages/service-core-tests/src/tests/util.ts: -------------------------------------------------------------------------------- 1 | import { test_utils } from '../index.js'; 2 | 3 | export const TEST_TABLE = test_utils.makeTestTable('test', ['id']); 4 | -------------------------------------------------------------------------------- /packages/service-core/src/modules/modules-index.ts: -------------------------------------------------------------------------------- 1 | export * from './ModuleManager.js'; 2 | export * from './AbstractModule.js'; 3 | export * from './loader.js'; 4 | -------------------------------------------------------------------------------- /packages/service-core/src/streams/streams-index.ts: -------------------------------------------------------------------------------- 1 | export * from './merge.js'; 2 | export * from './LastValueSink.js'; 3 | export * from './BroadcastIterable.js'; 4 | -------------------------------------------------------------------------------- /.changeset/ten-walls-cough.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@powersync/service-module-postgres': patch 3 | '@powersync/service-jpgwire': patch 4 | --- 5 | 6 | Update `pgwire` to version `0.8.1`. 7 | -------------------------------------------------------------------------------- /modules/module-mssql/dev/.env.template: -------------------------------------------------------------------------------- 1 | ROOT_PASSWORD=321strong_ROOT_password 2 | DATABASE=powersync 3 | DB_USER=powersync_user 4 | DB_USER_PASSWORD=321strong_POWERSYNC_password -------------------------------------------------------------------------------- /packages/service-core/src/api/api-index.ts: -------------------------------------------------------------------------------- 1 | export * from './diagnostics.js'; 2 | export * from './RouteAPI.js'; 3 | export * from './schema.js'; 4 | export * from './api-metrics.js'; 5 | -------------------------------------------------------------------------------- /.changeset/afraid-weeks-matter.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@powersync/service-module-postgres': patch 3 | --- 4 | 5 | Fix decoding arrays of enums, fix decoding `box[]` columns during initial replication. 6 | -------------------------------------------------------------------------------- /libs/lib-services/src/migrations/migrations-index.ts: -------------------------------------------------------------------------------- 1 | export * from './AbstractMigrationAgent.js'; 2 | export * from './migration-definitions.js'; 3 | export * from './MigrationManager.js'; 4 | -------------------------------------------------------------------------------- /packages/sync-rules/src/SourceTableInterface.ts: -------------------------------------------------------------------------------- 1 | export interface SourceTableInterface { 2 | readonly connectionTag: string; 3 | readonly schema: string; 4 | readonly name: string; 5 | } 6 | -------------------------------------------------------------------------------- /modules/module-mongodb/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function escapeRegExp(string: string) { 2 | // https://stackoverflow.com/a/3561711/214837 3 | return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); 4 | } 5 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' 3 | - 'libs/*' 4 | - 'modules/*' 5 | - 'service' 6 | - 'test-client' 7 | # exclude packages that are inside test directories 8 | - '!**/test/**' 9 | -------------------------------------------------------------------------------- /modules/module-mysql/dev/docker/mysql/init-scripts/my.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | gtid_mode = ON 3 | enforce-gtid-consistency = ON 4 | # Row format required for ZongJi 5 | binlog_format = row 6 | log_bin=mysql-bin 7 | server-id=1 -------------------------------------------------------------------------------- /packages/jsonbig/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './json.js'; 2 | export * from './json_container.js'; 3 | export { stringify as stringifyRaw } from './json_stringify.js'; 4 | export { Replacer } from 'lossless-json'; 5 | -------------------------------------------------------------------------------- /packages/service-core/test/src/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | export const env = utils.collectEnvironmentVariables({ 4 | CI: utils.type.boolean.default('false') 5 | }); 6 | -------------------------------------------------------------------------------- /packages/sync-rules/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { 6 | setupFiles: ['test/matchers.ts'] 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /libs/lib-services/src/signals/signals-index.ts: -------------------------------------------------------------------------------- 1 | export * from './probes/fs-probes.js'; 2 | export * from './probes/memory-probes.js'; 3 | export * from './probes/probes.js'; 4 | export * from './termination-handler.js'; 5 | -------------------------------------------------------------------------------- /libs/lib-services/src/alerts/no-op-reporter.ts: -------------------------------------------------------------------------------- 1 | import { ErrorReporter } from './definitions.js'; 2 | 3 | export const NoOpReporter: ErrorReporter = { 4 | captureException: () => {}, 5 | captureMessage: () => {} 6 | }; 7 | -------------------------------------------------------------------------------- /modules/module-mongodb/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api/MongoRouteAPIAdapter.js'; 2 | export * from './module/MongoModule.js'; 3 | export * from './replication/replication-index.js'; 4 | export * from './types/types.js'; 5 | -------------------------------------------------------------------------------- /packages/rsocket-router/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils/BaseObserver.js'; 2 | export * from './router/ReactiveSocketRouter.js'; 3 | export * from './router/SocketRouterListener.js'; 4 | export * from './router/types.js'; 5 | -------------------------------------------------------------------------------- /packages/service-core-tests/src/test-utils/test-utils-index.ts: -------------------------------------------------------------------------------- 1 | export * from './bucket-validation.js'; 2 | export * from './general-utils.js'; 3 | export * from './MetricsHelper.js'; 4 | export * from './stream_utils.js'; 5 | -------------------------------------------------------------------------------- /.changeset/nasty-snakes-double.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@powersync/service-core': patch 3 | '@powersync/service-image': patch 4 | --- 5 | 6 | - Rework dynamic module loading, fixing startup issues for migration and compact jobs in 1.18.0 / 1.18.1 7 | -------------------------------------------------------------------------------- /packages/service-core-tests/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Core Tests 2 | 3 | A small helper package which exposes common unit tests and test utility functions. 4 | 5 | This package is used in various modules for their unit tests. 6 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Technical Documentation 2 | 3 | This folder contains technical documentation regarding the implementation of PowerSync. 4 | 5 | For documentation on using PowerSync, see [docs.powersync.com](https://docs.powersync.com/). 6 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/migrations/db/migrations/1688556755264-initial-sync-rules.ts: -------------------------------------------------------------------------------- 1 | export const up = async () => { 2 | // No-op - we don't auto-create sync rules anymore 3 | }; 4 | 5 | export const down = async () => {}; 6 | -------------------------------------------------------------------------------- /modules/module-mongodb/src/replication/replication-index.ts: -------------------------------------------------------------------------------- 1 | export * from './MongoRelation.js'; 2 | export * from './ChangeStream.js'; 3 | export * from './ChangeStreamReplicator.js'; 4 | export * from './ChangeStreamReplicationJob.js'; 5 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/utils/utils-index.ts: -------------------------------------------------------------------------------- 1 | export * from './bson.js'; 2 | export * from './bucket-data.js'; 3 | export * from './db.js'; 4 | export * from './ts-codec.js'; 5 | export * as test_utils from './test-utils.js'; 6 | -------------------------------------------------------------------------------- /packages/jpgwire/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pgwire.js'; 2 | export * from './certs.js'; 3 | export * from './util.js'; 4 | export * from './metrics.js'; 5 | export * from './pgwire_types.js'; 6 | export * from './structure_parser.js'; 7 | -------------------------------------------------------------------------------- /packages/service-core/src/replication/ErrorRateLimiter.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorRateLimiter { 2 | waitUntilAllowed(options?: { signal?: AbortSignal }): Promise; 3 | reportError(e: any): void; 4 | 5 | mayPing(): boolean; 6 | } 7 | -------------------------------------------------------------------------------- /packages/service-core/test/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { container } from '@powersync/lib-services-framework'; 2 | import { beforeAll } from 'vitest'; 3 | 4 | beforeAll(() => { 5 | // Executes for every test file 6 | container.registerDefaults(); 7 | }); 8 | -------------------------------------------------------------------------------- /service/local-dev/sync-rules-template.yaml: -------------------------------------------------------------------------------- 1 | # See Documentation for more information: 2 | # https://docs.powersync.com/usage/sync-rules 3 | bucket_definitions: 4 | global: 5 | data: 6 | - SELECT * FROM lists 7 | - SELECT * FROM todos 8 | -------------------------------------------------------------------------------- /libs/lib-postgres/src/db/db-index.ts: -------------------------------------------------------------------------------- 1 | export * from './connection/AbstractPostgresConnection.js'; 2 | export * from './connection/ConnectionSlot.js'; 3 | export * from './connection/DatabaseClient.js'; 4 | export * from './connection/WrappedConnection.js'; 5 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/storage/storage-index.ts: -------------------------------------------------------------------------------- 1 | export * from './PostgresBucketStorageFactory.js'; 2 | export * from './PostgresCompactor.js'; 3 | export * from './PostgresStorageProvider.js'; 4 | export * from './PostgresSyncRulesStorage.js'; 5 | -------------------------------------------------------------------------------- /modules/module-mysql/src/common/common-index.ts: -------------------------------------------------------------------------------- 1 | export * from './check-source-configuration.js'; 2 | export * from './schema-utils.js'; 3 | export * from './mysql-to-sqlite.js'; 4 | export * from './read-executed-gtid.js'; 5 | export * from './ReplicatedGTID.js'; 6 | -------------------------------------------------------------------------------- /packages/service-core-tests/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './test-utils/test-utils-index.js'; 2 | export * as test_utils from './test-utils/test-utils-index.js'; 3 | 4 | export * from './tests/tests-index.js'; 5 | export * as register from './tests/tests-index.js'; 6 | -------------------------------------------------------------------------------- /packages/service-core/src/sync/sync-index.ts: -------------------------------------------------------------------------------- 1 | export * from './RequestTracker.js'; 2 | export * from './safeRace.js'; 3 | export * from './sync.js'; 4 | export * from './util.js'; 5 | export * from './BucketChecksumState.js'; 6 | export * from './SyncContext.js'; 7 | -------------------------------------------------------------------------------- /modules/module-postgres/src/replication/replication-index.ts: -------------------------------------------------------------------------------- 1 | export * from './PgRelation.js'; 2 | export * from './replication-utils.js'; 3 | export * from './WalStream.js'; 4 | export * from './WalStreamReplicator.js'; 5 | export * from './WalStreamReplicationJob.js'; 6 | -------------------------------------------------------------------------------- /packages/service-core/src/util/alerting.ts: -------------------------------------------------------------------------------- 1 | let globalTags: Record = {}; 2 | 3 | export function setTags(tags: Record) { 4 | globalTags = tags; 5 | } 6 | 7 | export function getGlobalTags() { 8 | return globalTags; 9 | } 10 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src", 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/service-errors/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"], 9 | "references": [] 10 | } 11 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/Instance.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | 3 | export const Instance = t.object({ 4 | id: t.string 5 | }); 6 | 7 | export type Instance = t.Encoded; 8 | export type InstanceDecoded = t.Decoded; 9 | -------------------------------------------------------------------------------- /packages/service-core/src/routes/endpoints/route-endpoints-index.ts: -------------------------------------------------------------------------------- 1 | export * from './admin.js'; 2 | export * from './checkpointing.js'; 3 | export * from './probes.js'; 4 | export * from './socket-route.js'; 5 | export * from './sync-rules.js'; 6 | export * from './sync-stream.js'; 7 | -------------------------------------------------------------------------------- /libs/lib-services/src/schema/schema-index.ts: -------------------------------------------------------------------------------- 1 | export * from './definitions.js'; 2 | 3 | export * from './json-schema/keywords.js'; 4 | export * from './json-schema/parser.js'; 5 | export * from './validators/schema-validator.js'; 6 | export * from './validators/ts-codec-validator.js'; 7 | -------------------------------------------------------------------------------- /packages/service-core/src/metrics/metrics-index.ts: -------------------------------------------------------------------------------- 1 | export * from './MetricsEngine.js'; 2 | export * from './metrics-interfaces.js'; 3 | export * from './register-metrics.js'; 4 | export * from './open-telemetry/OpenTelemetryMetricsFactory.js'; 5 | export * from './open-telemetry/util.js'; 6 | -------------------------------------------------------------------------------- /packages/service-core/src/util/lsn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return the larger of two LSNs. 3 | */ 4 | export function maxLsn(a: string | null | undefined, b: string | null | undefined): string | null { 5 | if (a == null) return b ?? null; 6 | if (b == null) return a; 7 | return a > b ? a : b; 8 | } 9 | -------------------------------------------------------------------------------- /libs/lib-services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"], 9 | "references": [{ "path": "../../packages/service-errors" }] 10 | } 11 | -------------------------------------------------------------------------------- /modules/module-postgres/src/utils/application-name.ts: -------------------------------------------------------------------------------- 1 | import { POWERSYNC_VERSION } from '@powersync/service-core'; 2 | 3 | /** 4 | * application_name for PostgreSQL connections to the source database 5 | */ 6 | export function getApplicationName() { 7 | return `powersync/${POWERSYNC_VERSION}`; 8 | } 9 | -------------------------------------------------------------------------------- /packages/types/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as configFile from './config/PowerSyncConfig.js'; 2 | 3 | export * from './definitions.js'; 4 | export * as internal_routes from './routes.js'; 5 | export * from './metrics.js'; 6 | export * as metric_types from './metrics.js'; 7 | export * as event_types from './reports'; 8 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/test/src/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | export const env = utils.collectEnvironmentVariables({ 4 | MONGO_TEST_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test'), 5 | CI: utils.type.boolean.default('false') 6 | }); 7 | -------------------------------------------------------------------------------- /packages/sync-rules/src/wkx.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@syncpoint/wkx' { 2 | export class Geometry { 3 | static parse(blob: any): Geometry | null; 4 | toGeoJSON(): any; 5 | toWkt(): string; 6 | } 7 | 8 | export class Point extends Geometry { 9 | x: number; 10 | y: number; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/jsonbig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"], 9 | "references": [ 10 | { 11 | "path": "../types" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # Connections for tests 2 | MONGO_TEST_URL="mongodb://localhost:27017/powersync_test" 3 | PG_TEST_URL="postgres://postgres:postgres@localhost:5432/powersync_test" 4 | # Note that this uses a separate server on a different port 5 | PG_STORAGE_TEST_URL="postgres://postgres:postgres@localhost:5431/powersync_storage_test" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | .swc 5 | 6 | node_modules 7 | dist 8 | 9 | .env 10 | **/*.tsbuildinfo 11 | **/lib 12 | */public 13 | 14 | npm-error.log 15 | .pnpm-debug.log 16 | .local-dev 17 | .probes 18 | powersync.yaml 19 | sync-rules.yaml 20 | 21 | 22 | packages/*/manifest.json 23 | 24 | .clinic -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/utils/application-name.ts: -------------------------------------------------------------------------------- 1 | import { POWERSYNC_VERSION } from '@powersync/service-core'; 2 | 3 | /** 4 | * Name for postgres application_name, for bucket storage connections. 5 | */ 6 | export function getStorageApplicationName() { 7 | return `powersync-storage/${POWERSYNC_VERSION}`; 8 | } 9 | -------------------------------------------------------------------------------- /packages/service-core/src/entry/entry-index.ts: -------------------------------------------------------------------------------- 1 | export * from './cli-entry.js'; 2 | export * from './commands/config-command.js'; 3 | export * from './commands/migrate-action.js'; 4 | export * from './commands/start-action.js'; 5 | export * from './commands/teardown-action.js'; 6 | export * from './commands/compact-action.js'; 7 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/defaults.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_MAX_POOL_SIZE = 8; 2 | export const DEFAULT_MAX_CONCURRENT_CONNECTIONS = 200; 3 | export const DEFAULT_MAX_DATA_FETCH_CONCURRENCY = 10; 4 | export const DEFAULT_MAX_BUCKETS_PER_CONNECTION = 1000; 5 | export const DEFAULT_MAX_PARAMETER_QUERY_RESULTS = 1000; 6 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/test/src/storage_compacting.test.ts: -------------------------------------------------------------------------------- 1 | import { register } from '@powersync/service-core-tests'; 2 | import { describe } from 'vitest'; 3 | import { POSTGRES_STORAGE_FACTORY } from './util.js'; 4 | 5 | describe('Postgres Sync Bucket Storage Compact', () => register.registerCompactTests(POSTGRES_STORAGE_FACTORY)); 6 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | setupFiles: './test/src/setup.ts', 6 | poolOptions: { 7 | threads: { 8 | singleThread: true 9 | } 10 | }, 11 | pool: 'threads' 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module/MongoStorageModule.js'; 2 | 3 | export * from './storage/storage-index.js'; 4 | export * as storage from './storage/storage-index.js'; 5 | 6 | export * from './types/types.js'; 7 | export * as types from './types/types.js'; 8 | export * as utils from './utils/utils-index.js'; 9 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/test/src/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | export const env = utils.collectEnvironmentVariables({ 4 | PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5431/powersync_storage_test'), 5 | CI: utils.type.boolean.default('false') 6 | }); 7 | -------------------------------------------------------------------------------- /modules/module-postgres/test/src/__snapshots__/schema_changes.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`schema changes > mongodb storage > add to publication (not in sync rules) 1`] = `0`; 4 | 5 | exports[`schema changes > postgres storage > add to publication (not in sync rules) 1`] = `16384`; 6 | -------------------------------------------------------------------------------- /packages/rsocket-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"], 9 | "references": [ 10 | { 11 | "path": "../../libs/lib-services" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /libs/lib-mongodb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [{ "path": "../lib-services" }] 12 | } 13 | -------------------------------------------------------------------------------- /packages/service-core/src/auth/JwtPayload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Payload from a JWT, always signed. 3 | * 4 | * May have arbitrary additional parameters. 5 | */ 6 | export interface JwtPayload { 7 | /** 8 | * token_parameters.user_id 9 | */ 10 | sub: string; 11 | 12 | iss?: string | undefined; 13 | exp: number; 14 | iat: number; 15 | } 16 | -------------------------------------------------------------------------------- /test-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["ES2023", "DOM"], 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "esModuleInterop": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [{ "path": "../packages/service-core" }] 12 | } 13 | -------------------------------------------------------------------------------- /packages/sync-rules/test/matchers.d.ts: -------------------------------------------------------------------------------- 1 | import 'vitest'; 2 | 3 | interface CustomMatchers { 4 | toBeSqlRuleError: (message: string, location: string) => R; 5 | } 6 | 7 | declare module 'vitest' { 8 | interface Assertion extends CustomMatchers {} 9 | interface AsymmetricMatchersContaining extends CustomMatchers {} 10 | } 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "printWidth": 120, 7 | "trailingComma": "none", 8 | "plugins": ["prettier-plugin-embed", "prettier-plugin-sql"], 9 | "embeddedSqlTags": ["sql", "db.sql", "this.db.sql"], 10 | "language": "postgresql", 11 | "keywordCase": "upper" 12 | } 13 | -------------------------------------------------------------------------------- /libs/lib-services/src/signals/probes/probes.ts: -------------------------------------------------------------------------------- 1 | export type ProbeState = { 2 | ready: boolean; 3 | started: boolean; 4 | touched_at: Date; 5 | }; 6 | 7 | export type ProbeModule = { 8 | poll_timeout: number; 9 | 10 | state(): ProbeState; 11 | 12 | ready(): Promise; 13 | unready(): Promise; 14 | touch(): Promise; 15 | }; 16 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/test/src/__snapshots__/storage.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Postgres Sync Bucket Storage - Data > empty storage metrics 1`] = ` 4 | { 5 | "operations_size_bytes": 16384, 6 | "parameters_size_bytes": 32768, 7 | "replication_size_bytes": 16384, 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /packages/service-core/src/replication/replication-index.ts: -------------------------------------------------------------------------------- 1 | export * from './AbstractReplicationJob.js'; 2 | export * from './AbstractReplicator.js'; 3 | export * from './ErrorRateLimiter.js'; 4 | export * from './ReplicationEngine.js'; 5 | export * from './ReplicationModule.js'; 6 | export * from './replication-metrics.js'; 7 | export * from './RelationCache.js'; 8 | -------------------------------------------------------------------------------- /modules/module-core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | poolOptions: { 8 | threads: { 9 | singleThread: true 10 | } 11 | }, 12 | pool: 'threads' 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /packages/jsonbig/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @powersync/service-jsonbig 2 | 3 | ## 0.17.12 4 | 5 | ### Patch Changes 6 | 7 | - 88982d9: Migrate to trusted publishing 8 | 9 | ## 0.17.11 10 | 11 | ### Patch Changes 12 | 13 | - 060b829: Update license abbreviation to FSL-1.1-ALv2. 14 | 15 | ## 0.17.10 16 | 17 | ### Patch Changes 18 | 19 | - 285f368: Initial public release 20 | -------------------------------------------------------------------------------- /libs/lib-services/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "noEmit": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/sync-rules/src/streams/utils.ts: -------------------------------------------------------------------------------- 1 | export function* cartesianProduct(...sets: T[][]): Generator { 2 | if (sets.length == 0) { 3 | yield []; 4 | return; 5 | } 6 | 7 | const [head, ...tail] = sets; 8 | for (let h of head) { 9 | const remainder = cartesianProduct(...tail); 10 | for (let r of remainder) yield [h, ...r]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/test/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { container } from '@powersync/lib-services-framework'; 2 | import { beforeAll } from 'vitest'; 3 | import { METRICS_HELPER } from '@powersync/service-core-tests'; 4 | 5 | beforeAll(async () => { 6 | // Executes for every test file 7 | container.registerDefaults(); 8 | 9 | METRICS_HELPER.resetMetrics(); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/schema/README.md: -------------------------------------------------------------------------------- 1 | # PowerSync Service Schema 2 | 3 | This package includes a JSON Schema for the PowerSync service configuration file. 4 | 5 | This can be used to provide validations and suggestions for configuration files 6 | 7 | ```yaml 8 | # yaml-language-server: $schema=https://unpkg.com/@powersync/service-schema@latest/json-schema/powersync-config.json 9 | ``` 10 | -------------------------------------------------------------------------------- /libs/lib-postgres/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [{ "path": "../lib-services" }, { "path": "../../packages/jpgwire" }] 12 | } 13 | -------------------------------------------------------------------------------- /modules/module-mysql/dev/config/sync_rules.yaml: -------------------------------------------------------------------------------- 1 | # See Documentation for more information: 2 | # https://docs.powersync.com/usage/sync-rules 3 | # Note that changes to this file are not watched. 4 | # The service needs to be restarted for changes to take effect. 5 | 6 | bucket_definitions: 7 | global: 8 | data: 9 | - SELECT * FROM lists 10 | - SELECT * FROM todos 11 | -------------------------------------------------------------------------------- /packages/rsocket-router/tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "noEmit": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/jpgwire/README.md: -------------------------------------------------------------------------------- 1 | # jpgwire 2 | 3 | pgwire is a nice low-level lib that supports postgres streaming replication. 4 | 5 | Unfortunately, it is not very mature, and we need some modifications. 6 | 7 | We wrap the lib, and customize some parts of it here. 8 | 9 | Additionally, this includes wrappers to connect using PowerSync config, as well as a set of standard CA certificates. 10 | -------------------------------------------------------------------------------- /libs/lib-services/test/src/__snapshots__/errors.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`errors > utilities should properly match a service error 1`] = ` 4 | { 5 | "code": "PSYNC_S0001", 6 | "description": "This is a custom error", 7 | "details": "this is some more detailed information", 8 | "name": "CustomServiceError", 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /packages/jpgwire/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "noEmit": true, 6 | "baseUrl": "./", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true 10 | }, 11 | "include": ["src"], 12 | "references": [ 13 | { 14 | "path": "../" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/sync-rules/sync-collector.ts: -------------------------------------------------------------------------------- 1 | import { configFile } from '@powersync/service-types'; 2 | import { RunnerConfig, SyncRulesConfig } from '../types.js'; 3 | 4 | export abstract class SyncRulesCollector { 5 | abstract get name(): string; 6 | 7 | abstract collect(baseConfig: configFile.PowerSyncConfig, runnerConfig: RunnerConfig): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /libs/lib-postgres/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './db/db-index.js'; 2 | export * as db from './db/db-index.js'; 3 | 4 | export * from './locks/locks-index.js'; 5 | export * as locks from './locks/locks-index.js'; 6 | 7 | export * from './types/types.js'; 8 | export * as types from './types/types.js'; 9 | 10 | export * from './utils/utils-index.js'; 11 | export * as utils from './utils/utils-index.js'; 12 | -------------------------------------------------------------------------------- /libs/lib-services/test/__snapshots__/errors.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`errors > utilities should properly match a journey error 1`] = ` 4 | { 5 | "code": "CUSTOM_JOURNEY_ERROR", 6 | "description": "This is a custom error", 7 | "details": "this is some more detailed information", 8 | "name": "CustomJourneyError", 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /packages/sync-rules/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "noEmit": true, 6 | "baseUrl": "./", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true 10 | }, 11 | "include": ["src"], 12 | "references": [ 13 | { 14 | "path": "../" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /service/src/util/version.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@powersync/lib-services-framework'; 2 | 3 | import { POWERSYNC_VERSION } from '@powersync/service-core'; 4 | 5 | export function logBooting(runner: string) { 6 | const version = POWERSYNC_VERSION; 7 | const edition = 'Open Edition'; 8 | logger.info(`Booting PowerSync Service v${version}, ${runner}, ${edition}`, { version, edition, runner }); 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2024"], 4 | "target": "ES2024", 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "strict": true, 8 | "composite": true, 9 | "declaration": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "sourceMap": true, 13 | "useUnknownInCatchVariables": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/module-mongodb/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | setupFiles: './test/src/setup.ts', 8 | poolOptions: { 9 | threads: { 10 | singleThread: true 11 | } 12 | }, 13 | pool: 'threads' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /modules/module-mssql/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | setupFiles: './test/src/setup.ts', 8 | poolOptions: { 9 | threads: { 10 | singleThread: true 11 | } 12 | }, 13 | pool: 'threads' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /modules/module-mysql/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | setupFiles: './test/src/setup.ts', 8 | poolOptions: { 9 | threads: { 10 | singleThread: true 11 | } 12 | }, 13 | pool: 'threads' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module/PostgresStorageModule.js'; 2 | 3 | export * from './migrations/PostgresMigrationAgent.js'; 4 | 5 | export * from './utils/utils-index.js'; 6 | export * as utils from './utils/utils-index.js'; 7 | 8 | export * from './storage/storage-index.js'; 9 | export * as storage from './storage/storage-index.js'; 10 | export * from './types/types.js'; 11 | -------------------------------------------------------------------------------- /modules/module-postgres/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | setupFiles: './test/src/setup.ts', 8 | poolOptions: { 9 | threads: { 10 | singleThread: true 11 | } 12 | }, 13 | pool: 'threads' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /packages/service-core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | setupFiles: './test/src/setup.ts', 8 | poolOptions: { 9 | threads: { 10 | singleThread: true 11 | } 12 | }, 13 | pool: 'threads' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /modules/module-mssql/test/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { container } from '@powersync/lib-services-framework'; 2 | import { METRICS_HELPER } from '@powersync/service-core-tests'; 3 | import { beforeAll, beforeEach } from 'vitest'; 4 | 5 | beforeAll(async () => { 6 | // Executes for every test file 7 | container.registerDefaults(); 8 | }); 9 | 10 | beforeEach(async () => { 11 | METRICS_HELPER.resetMetrics(); 12 | }); 13 | -------------------------------------------------------------------------------- /modules/module-mysql/test/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { container } from '@powersync/lib-services-framework'; 2 | import { METRICS_HELPER } from '@powersync/service-core-tests'; 3 | import { beforeAll, beforeEach } from 'vitest'; 4 | 5 | beforeAll(async () => { 6 | // Executes for every test file 7 | container.registerDefaults(); 8 | }); 9 | 10 | beforeEach(async () => { 11 | METRICS_HELPER.resetMetrics(); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/lib-mongodb/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": {} 11 | }, 12 | "include": ["src"], 13 | "references": [ 14 | { 15 | "path": "../" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/lib-postgres/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": {} 11 | }, 12 | "include": ["src"], 13 | "references": [ 14 | { 15 | "path": "../" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | test: { 7 | setupFiles: './test/src/setup.ts', 8 | poolOptions: { 9 | threads: { 10 | singleThread: true 11 | } 12 | }, 13 | pool: 'threads' 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /modules/module-postgres/test/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { container } from '@powersync/lib-services-framework'; 2 | import { METRICS_HELPER } from '@powersync/service-core-tests'; 3 | import { beforeAll, beforeEach } from 'vitest'; 4 | 5 | beforeAll(async () => { 6 | // Executes for every test file 7 | container.registerDefaults(); 8 | }); 9 | 10 | beforeEach(async () => { 11 | METRICS_HELPER.resetMetrics(); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/sync-rules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src", 7 | "esModuleInterop": true, 8 | "typeRoots": ["./src/wkx.d.ts", "./node_modules/@types"] 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../jsonbig" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/test/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { container } from '@powersync/lib-services-framework'; 2 | import { beforeAll, beforeEach } from 'vitest'; 3 | import { METRICS_HELPER } from '@powersync/service-core-tests'; 4 | 5 | beforeAll(async () => { 6 | // Executes for every test file 7 | container.registerDefaults(); 8 | }); 9 | 10 | beforeEach(async () => { 11 | METRICS_HELPER.resetMetrics(); 12 | }); 13 | -------------------------------------------------------------------------------- /libs/lib-mongodb/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './db/db-index.js'; 2 | export * as db from './db/db-index.js'; 3 | 4 | export * from './locks/locks-index.js'; 5 | export * as locks from './locks/locks-index.js'; 6 | 7 | export * from './types/types.js'; 8 | export * as types from './types/types.js'; 9 | 10 | // Re-export mongodb which can avoid using multiple versions of Mongo in a project 11 | export * as mongo from 'mongodb'; 12 | -------------------------------------------------------------------------------- /packages/service-core/src/routes/routes-index.ts: -------------------------------------------------------------------------------- 1 | export * as auth from './auth.js'; 2 | export * from './configure-fastify.js'; 3 | export * from './configure-rsocket.js'; 4 | export * as endpoints from './endpoints/route-endpoints-index.js'; 5 | export * as hooks from './hooks.js'; 6 | export * from './route-register.js'; 7 | export * from './router-socket.js'; 8 | export * from './router.js'; 9 | export * from './RouterEngine.js'; 10 | -------------------------------------------------------------------------------- /libs/lib-mongodb/src/types/mongodb-connection-string-url.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'mongodb-connection-string-url' { 2 | export class ConnectionURI { 3 | constructor(uri: string); 4 | toString(): string; 5 | searchParams: URLSearchParams; 6 | pathname: string; 7 | username: string; 8 | password: string; 9 | hosts: string[]; 10 | isSRV: boolean; 11 | href: string; 12 | } 13 | export default ConnectionURI; 14 | } 15 | -------------------------------------------------------------------------------- /modules/module-mongodb/test/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { container } from '@powersync/lib-services-framework'; 2 | import { METRICS_HELPER } from '@powersync/service-core-tests'; 3 | import { beforeEach } from 'node:test'; 4 | import { beforeAll } from 'vitest'; 5 | 6 | beforeAll(async () => { 7 | // Executes for every test file 8 | container.registerDefaults(); 9 | }); 10 | 11 | beforeEach(async () => { 12 | METRICS_HELPER.resetMetrics(); 13 | }); 14 | -------------------------------------------------------------------------------- /modules/module-mysql/dev/README.md: -------------------------------------------------------------------------------- 1 | # MySQL Development Helpers 2 | 3 | This folder contains some helpers for developing with MySQL. 4 | 5 | - `./.env.template` contains basic settings to be applied to a root `.env` file 6 | - `./config` contains YAML configuration files for a MySQL todo list application 7 | - `./docker/mysql` contains a docker compose file for starting Mysql 8 | 9 | TODO this does not contain any auth or backend functionality. 10 | -------------------------------------------------------------------------------- /packages/service-core/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "outDir": "dist", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../src/*"] 12 | } 13 | }, 14 | "include": ["src"], 15 | "references": [ 16 | { 17 | "path": "../" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /modules/module-postgres/src/utils/postgres_version.ts: -------------------------------------------------------------------------------- 1 | import * as pgwire from '@powersync/service-jpgwire'; 2 | import semver, { type SemVer } from 'semver'; 3 | 4 | export async function getServerVersion(db: pgwire.PgClient): Promise { 5 | const result = await db.query(`SHOW server_version;`); 6 | // The result is usually of the form "16.2 (Debian 16.2-1.pgdg120+2)" 7 | return semver.coerce(result.rows[0].decodeWithoutCustomTypes(0).split(' ')[0]); 8 | } 9 | -------------------------------------------------------------------------------- /packages/service-errors/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "outDir": "dist", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../src/*"] 12 | } 13 | }, 14 | "include": ["src"], 15 | "references": [ 16 | { 17 | "path": "../" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/jpgwire/src/metrics.ts: -------------------------------------------------------------------------------- 1 | export interface MetricsRecorder { 2 | addBytesRead(bytes: number): void; 3 | } 4 | 5 | let metricsRecorder: MetricsRecorder | undefined; 6 | 7 | /** 8 | * Configure a metrics recorder to capture global metrics. 9 | */ 10 | export function setMetricsRecorder(recorder: MetricsRecorder) { 11 | metricsRecorder = recorder; 12 | } 13 | 14 | export function recordBytesRead(bytes: number) { 15 | metricsRecorder?.addBytesRead(bytes); 16 | } 17 | -------------------------------------------------------------------------------- /libs/lib-postgres/src/db/connection/WrappedConnection.ts: -------------------------------------------------------------------------------- 1 | import * as pgwire from '@powersync/service-jpgwire'; 2 | import { AbstractPostgresConnection } from './AbstractPostgresConnection.js'; 3 | 4 | /** 5 | * Provides helper functionality to transaction contexts given an existing PGWire connection 6 | */ 7 | export class WrappedConnection extends AbstractPostgresConnection { 8 | constructor(protected baseConnection: pgwire.PgConnection) { 9 | super(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/service-core/src/auth/auth-index.ts: -------------------------------------------------------------------------------- 1 | export * from './CachedKeyCollector.js'; 2 | export * from './CompoundKeyCollector.js'; 3 | export * from './JwtPayload.js'; 4 | export * from './KeyCollector.js'; 5 | export * from './KeySpec.js'; 6 | export * from './KeyStore.js'; 7 | export * from './LeakyBucket.js'; 8 | export * from './RemoteJWKSCollector.js'; 9 | export * from './StaticKeyCollector.js'; 10 | export * from './StaticSupabaseKeyCollector.js'; 11 | export * from './utils.js'; 12 | -------------------------------------------------------------------------------- /packages/sync-rules/scripts/compile-schema.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { syncRulesSchema } from '../dist/json_schema.js'; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | const schemaDir = path.join(__dirname, '../schema'); 8 | 9 | fs.mkdirSync(schemaDir, { recursive: true }); 10 | 11 | fs.writeFileSync(path.join(schemaDir, 'sync_rules.json'), JSON.stringify(syncRulesSchema, null, '\t')); 12 | -------------------------------------------------------------------------------- /libs/lib-postgres/test/src/config.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { normalizeConnectionConfig } from '../../src/types/types.js'; 3 | 4 | describe('config', () => { 5 | test('Should resolve database', () => { 6 | const normalized = normalizeConnectionConfig({ 7 | type: 'postgresql', 8 | uri: 'postgresql://postgres:postgres@localhost:4321/powersync_test' 9 | }); 10 | expect(normalized.database).equals('powersync_test'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/models-index.ts: -------------------------------------------------------------------------------- 1 | export * from './ActiveCheckpoint.js'; 2 | export * from './ActiveCheckpointNotification.js'; 3 | export * from './BucketData.js'; 4 | export * from './BucketParameters.js'; 5 | export * from './CurrentData.js'; 6 | export * from './Instance.js'; 7 | export * from './Migration.js'; 8 | export * from './SourceTable.js'; 9 | export * from './SyncRules.js'; 10 | export * from './WriteCheckpoint.js'; 11 | export * from './SdkReporting.js'; 12 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/test/src/storage_sync.test.ts: -------------------------------------------------------------------------------- 1 | import { register } from '@powersync/service-core-tests'; 2 | import { describe } from 'vitest'; 3 | import { POSTGRES_STORAGE_FACTORY } from './util.js'; 4 | 5 | /** 6 | * Bucket compacting is not yet implemented. 7 | * This causes the internal compacting test to fail. 8 | * Other tests have been verified manually. 9 | */ 10 | describe('sync - postgres', () => { 11 | register.registerSyncTests(POSTGRES_STORAGE_FACTORY); 12 | }); 13 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [["@powersync/service-core", "@powersync/service-image", "@powersync/service-schema"]], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "origin/main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [], 11 | "privatePackages": { 12 | "tag": true, 13 | "version": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/jpgwire/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src", 7 | "esModuleInterop": true, 8 | "allowJs": true, 9 | "skipLibCheck": true 10 | }, 11 | "include": ["src"], 12 | "references": [ 13 | { 14 | "path": "../types" 15 | }, 16 | { 17 | "path": "../jsonbig" 18 | }, 19 | { 20 | "path": "../sync-rules" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/test/src/util.ts: -------------------------------------------------------------------------------- 1 | import { env } from './env.js'; 2 | import { mongoTestReportStorageFactoryGenerator, mongoTestStorageFactoryGenerator } from '@module/utils/test-utils.js'; 3 | 4 | export const INITIALIZED_MONGO_STORAGE_FACTORY = mongoTestStorageFactoryGenerator({ 5 | url: env.MONGO_TEST_URL, 6 | isCI: env.CI 7 | }); 8 | 9 | export const INITIALIZED_MONGO_REPORT_STORAGE_FACTORY = mongoTestReportStorageFactoryGenerator({ 10 | url: env.MONGO_TEST_URL, 11 | isCI: env.CI 12 | }); 13 | -------------------------------------------------------------------------------- /modules/module-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../../packages/types" 14 | }, 15 | { 16 | "path": "../../packages/service-core" 17 | }, 18 | { 19 | "path": "../../libs/lib-services" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/test/src/migrations.test.ts: -------------------------------------------------------------------------------- 1 | import { register } from '@powersync/service-core-tests'; 2 | import { describe } from 'vitest'; 3 | import { MongoMigrationAgent } from '../../src/migrations/MongoMigrationAgent.js'; 4 | import { env } from './env.js'; 5 | 6 | const MIGRATION_AGENT_FACTORY = () => { 7 | return new MongoMigrationAgent({ type: 'mongodb', uri: env.MONGO_TEST_URL }); 8 | }; 9 | 10 | describe('Mongo Migrations Store', () => register.registerMigrationTests(MIGRATION_AGENT_FACTORY)); 11 | -------------------------------------------------------------------------------- /packages/rsocket-router/src/router/SocketRouterListener.ts: -------------------------------------------------------------------------------- 1 | import { BaseObserver } from '../utils/BaseObserver.js'; 2 | 3 | export interface SocketRouterListener { 4 | onExtension: () => void; 5 | request: (n: number) => void; 6 | } 7 | 8 | export class SocketRouterObserver extends BaseObserver { 9 | triggerExtension() { 10 | this.iterateListeners((l) => l.onExtension?.()); 11 | } 12 | 13 | triggerRequest(n: number) { 14 | this.iterateListeners((l) => l.request?.(n)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/service-core/src/routes/router-socket.ts: -------------------------------------------------------------------------------- 1 | import { IReactiveStream, ReactiveSocketRouter } from '@powersync/service-rsocket-router'; 2 | import * as t from 'ts-codec'; 3 | 4 | import { Context } from './router.js'; 5 | 6 | /** 7 | * Creates a socket route handler given a router instance 8 | */ 9 | export type SocketRouteGenerator = (router: ReactiveSocketRouter) => IReactiveStream; 10 | 11 | export const RSocketContextMeta = t.object({ 12 | token: t.string, 13 | user_agent: t.string.optional() 14 | }); 15 | -------------------------------------------------------------------------------- /.changeset/short-pumpkins-retire.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@powersync/service-image': minor 3 | '@powersync/service-module-postgres': patch 4 | '@powersync/service-module-mongodb': patch 5 | '@powersync/service-module-mysql': patch 6 | '@powersync/service-module-mssql': patch 7 | '@powersync/service-sync-rules': patch 8 | '@powersync/service-jpgwire': patch 9 | --- 10 | 11 | Add the `timestamp_max_precision` option for sync rules. It can be set to `seconds`, `milliseconds` or `microseconds` to restrict the precision of synced datetime values. 12 | -------------------------------------------------------------------------------- /modules/module-mysql/dev/docker/mysql/init-scripts/mysql_57.sql: -------------------------------------------------------------------------------- 1 | -- Create a user with necessary privileges 2 | CREATE USER 'repl_user'@'%' IDENTIFIED BY 'good_password'; 3 | 4 | -- Grant replication client privilege 5 | GRANT REPLICATION SLAVE, REPLICATION CLIENT, RELOAD ON *.* TO 'repl_user'@'%'; 6 | GRANT REPLICATION SLAVE, REPLICATION CLIENT, RELOAD ON *.* TO 'myuser'@'%'; 7 | 8 | -- Grant access to the specific database 9 | GRANT ALL PRIVILEGES ON mydatabase.* TO 'repl_user'@'%'; 10 | 11 | -- Apply changes 12 | FLUSH PRIVILEGES; -------------------------------------------------------------------------------- /modules/module-mysql/dev/docker/mysql/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mysql: 3 | image: mysql:8.0 4 | environment: 5 | MYSQL_ROOT_PASSWORD: root_password 6 | MYSQL_DATABASE: mydatabase 7 | MYSQL_USER: myuser 8 | MYSQL_PASSWORD: mypassword 9 | ports: 10 | - '3306:3306' 11 | volumes: 12 | - ./init-scripts/my.cnf:/etc/mysql/my.cnf 13 | - ./init-scripts/mysql.sql:/docker-entrypoint-initdb.d/init_user.sql 14 | - mysql_data:/var/lib/mysql 15 | 16 | volumes: 17 | mysql_data: 18 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "declarationDir": "dist/@types", 9 | "tsBuildInfoFile": "dist/.tsbuildinfo", 10 | "lib": ["ES2022", "esnext.disposable"], 11 | "skipLibCheck": true, 12 | "sourceMap": true 13 | }, 14 | "include": ["src"], 15 | "references": [ 16 | { 17 | "path": "../" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/jpgwire/src/certs.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises'; 2 | import { resolve } from 'path'; 3 | 4 | export const DEFAULT_CERTS = await loadDefaultCertificates(); 5 | 6 | async function loadDefaultCertificates() { 7 | const dir = new URL('../ca', import.meta.url).pathname; 8 | const files = await fs.readdir(dir); 9 | let sum = ''; 10 | for (let file of files) { 11 | if (file.endsWith('.pem')) { 12 | sum += (await fs.readFile(resolve(dir, file), 'utf-8')) + '\n'; 13 | } 14 | } 15 | return sum; 16 | } 17 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/BucketParameters.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { bigint, hexBuffer, pgwire_number } from '../codecs.js'; 3 | 4 | export const BucketParameters = t.object({ 5 | id: bigint, 6 | group_id: pgwire_number, 7 | source_table: t.string, 8 | source_key: hexBuffer, 9 | lookup: hexBuffer, 10 | bucket_parameters: t.string 11 | }); 12 | 13 | export type BucketParameters = t.Encoded; 14 | export type BucketParametersDecoded = t.Decoded; 15 | -------------------------------------------------------------------------------- /modules/module-mysql/dev/docker/mysql/docker-compose-57.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mysql: 3 | image: mysql:5.7 4 | environment: 5 | MYSQL_ROOT_PASSWORD: root_password 6 | MYSQL_DATABASE: mydatabase 7 | MYSQL_USER: myuser 8 | MYSQL_PASSWORD: mypassword 9 | ports: 10 | - '3306:3306' 11 | volumes: 12 | - ./init-scripts/my.cnf:/etc/mysql/my.cnf 13 | - ./init-scripts/mysql_57.sql:/docker-entrypoint-initdb.d/init_user.sql 14 | - mysql_data_57:/var/lib/mysql 15 | 16 | volumes: 17 | mysql_data_57: 18 | -------------------------------------------------------------------------------- /packages/rsocket-router/tests/src/utils/mock-responder.ts: -------------------------------------------------------------------------------- 1 | import { SocketRouterObserver } from '../../../src/router/SocketRouterListener.js'; 2 | import { SocketResponder } from '../../../src/router/types.js'; 3 | 4 | export function createMockResponder(): SocketResponder { 5 | return { 6 | onComplete: () => {}, 7 | onError: (error) => {}, 8 | onExtension: () => {}, 9 | onNext: (payload, isComplete) => {} 10 | }; 11 | } 12 | 13 | export function createMockObserver(): SocketRouterObserver { 14 | return new SocketRouterObserver(); 15 | } 16 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-types", 3 | "version": "0.13.3", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "license": "FSL-1.1-ALv2", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "repository": "https://github.com/powersync-ja/powersync-service", 11 | "scripts": { 12 | "clean": "rm -r ./dist && tsc -b --clean", 13 | "build": "tsc -b" 14 | }, 15 | "dependencies": { 16 | "dedent": "^1.6.0", 17 | "ts-codec": "^1.3.0", 18 | "uri-js": "^4.4.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/ActiveCheckpoint.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { bigint, pgwire_number } from '../codecs.js'; 3 | 4 | /** 5 | * Notification payload sent via Postgres' NOTIFY API. 6 | * 7 | */ 8 | export const ActiveCheckpoint = t.object({ 9 | id: pgwire_number, 10 | last_checkpoint: t.Null.or(bigint), 11 | last_checkpoint_lsn: t.Null.or(t.string) 12 | }); 13 | 14 | export type ActiveCheckpoint = t.Encoded; 15 | export type ActiveCheckpointDecoded = t.Decoded; 16 | -------------------------------------------------------------------------------- /packages/service-core/src/storage/ReplicationEventPayload.ts: -------------------------------------------------------------------------------- 1 | import * as sync_rules from '@powersync/service-sync-rules'; 2 | import { SourceTable } from './SourceTable.js'; 3 | import { BucketStorageBatch, SaveOp } from './BucketStorageBatch.js'; 4 | 5 | export type EventData = { 6 | op: SaveOp; 7 | before?: sync_rules.SqliteInputRow; 8 | after?: sync_rules.SqliteInputRow; 9 | }; 10 | 11 | export type ReplicationEventPayload = { 12 | batch: BucketStorageBatch; 13 | data: EventData; 14 | event: sync_rules.SqlEventDescriptor; 15 | table: SourceTable; 16 | }; 17 | -------------------------------------------------------------------------------- /libs/lib-services/src/schema/definitions.ts: -------------------------------------------------------------------------------- 1 | export type JSONSchema = { 2 | definitions?: Record; 3 | [key: string]: any; 4 | }; 5 | 6 | export type IValidationRight = { 7 | valid: true; 8 | }; 9 | 10 | export type ValidationLeft = { 11 | valid: false; 12 | errors: T; 13 | }; 14 | 15 | export type ValidationResponse = ValidationLeft | IValidationRight; 16 | 17 | export type MicroValidator = { 18 | validate: (data: T) => ValidationResponse; 19 | toJSONSchema?: () => JSONSchema; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/service-core-tests/src/tests/tests-index.ts: -------------------------------------------------------------------------------- 1 | export * from './register-bucket-validation-tests.js'; 2 | export * from './register-compacting-tests.js'; 3 | export * from './register-parameter-compacting-tests.js'; 4 | export * from './register-data-storage-parameter-tests.js'; 5 | export * from './register-data-storage-data-tests.js'; 6 | export * from './register-data-storage-checkpoint-tests.js'; 7 | export * from './register-migration-tests.js'; 8 | export * from './register-sync-tests.js'; 9 | export * from './register-report-tests.js'; 10 | export * from './util.js'; 11 | -------------------------------------------------------------------------------- /modules/module-core/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../../../packages/service-core/src/*"], 12 | "@module/*": ["../src/*"], 13 | "@core-tests/*": ["../../../packages/service-core/test/src/*"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { 19 | "path": "../" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/storage/implementation/MongoPersistedSyncRules.ts: -------------------------------------------------------------------------------- 1 | import { SqlSyncRules } from '@powersync/service-sync-rules'; 2 | 3 | import { storage } from '@powersync/service-core'; 4 | 5 | export class MongoPersistedSyncRules implements storage.PersistedSyncRules { 6 | public readonly slot_name: string; 7 | 8 | constructor( 9 | public readonly id: number, 10 | public readonly sync_rules: SqlSyncRules, 11 | public readonly checkpoint_lsn: string | null, 12 | slot_name: string | null 13 | ) { 14 | this.slot_name = slot_name ?? `powersync_${id}`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/Migration.ts: -------------------------------------------------------------------------------- 1 | import { framework } from '@powersync/service-core'; 2 | import * as t from 'ts-codec'; 3 | import { jsonb } from '../codecs.js'; 4 | 5 | export const Migration = t.object({ 6 | last_run: t.string, 7 | log: jsonb( 8 | t.array( 9 | t.object({ 10 | name: t.string, 11 | direction: t.Enum(framework.migrations.Direction), 12 | timestamp: framework.codecs.date 13 | }) 14 | ) 15 | ) 16 | }); 17 | 18 | export type Migration = t.Encoded; 19 | export type MigrationDecoded = t.Decoded; 20 | -------------------------------------------------------------------------------- /packages/jsonbig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-jsonbig", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "version": "0.17.12", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "license": "FSL-1.1-ALv2", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "files": [ 12 | "dist/**/*" 13 | ], 14 | "type": "module", 15 | "scripts": { 16 | "clean": "rm -r ./dist && tsc -b --clean", 17 | "build": "tsc -b" 18 | }, 19 | "dependencies": { 20 | "lossless-json": "^2.0.8" 21 | }, 22 | "devDependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /modules/module-mssql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true, 9 | "typeRoots": ["./node_modules/@types"] 10 | }, 11 | "include": ["src"], 12 | "references": [ 13 | { 14 | "path": "../../packages/types" 15 | }, 16 | { 17 | "path": "../../packages/sync-rules" 18 | }, 19 | { 20 | "path": "../../packages/service-core" 21 | }, 22 | { 23 | "path": "../../libs/lib-services" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /libs/lib-services/test/src/__snapshots__/ts-codec-validation.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ts-codec validation > fails validation for runtime codec 1`] = ` 4 | { 5 | "errors": [ 6 | " must have required property 'surname'", 7 | "/name: type must be string", 8 | "/other/b: const must be equal to constant", 9 | "/tuple/0: type must be string", 10 | "/or: type must be number", 11 | "/or: type must be string", 12 | "/or: anyOf must match a schema in anyOf", 13 | "/complex must have required property 'c'", 14 | ], 15 | "valid": false, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /packages/service-core/src/migrations/ensure-automatic-migrations.ts: -------------------------------------------------------------------------------- 1 | import * as framework from '@powersync/lib-services-framework'; 2 | import * as system from '../system/system-index.js'; 3 | 4 | export const ensureAutomaticMigrations = async (options: { serviceContext: system.ServiceContext }) => { 5 | const { serviceContext } = options; 6 | if (serviceContext.configuration.migrations?.disable_auto_migration) { 7 | return; 8 | } 9 | await serviceContext.migrations.migrate({ 10 | direction: framework.migrations.Direction.Up, 11 | migrationContext: { 12 | service_context: serviceContext 13 | } 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/collectors/impl/base64-config-collector.ts: -------------------------------------------------------------------------------- 1 | import { ConfigCollector } from '../config-collector.js'; 2 | import { RunnerConfig } from '../../types.js'; 3 | 4 | export class Base64ConfigCollector extends ConfigCollector { 5 | get name(): string { 6 | return 'Base64'; 7 | } 8 | 9 | async collectSerialized(runnerConfig: RunnerConfig) { 10 | const { config_base64 } = runnerConfig; 11 | if (!config_base64) { 12 | return null; 13 | } 14 | 15 | // Could be JSON or YAML at this point 16 | return this.parseContent(Buffer.from(config_base64, 'base64').toString()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/lib-services/test/src/schema/__snapshots__/ts-codec-validation.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`ts-codec validation > fails validation for runtime codec 1`] = ` 4 | { 5 | "errors": [ 6 | " must have required property 'surname'", 7 | "/name: type must be string", 8 | "/other/b: const must be equal to constant", 9 | "/tuple/0: type must be string", 10 | "/or: type must be number", 11 | "/or: type must be string", 12 | "/or: anyOf must match a schema in anyOf", 13 | "/complex must have required property 'c'", 14 | ], 15 | "valid": false, 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/utils/ts-codec.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | 3 | /** 4 | * Returns a new codec with a subset of keys. Equivalent to the TypeScript Pick utility. 5 | */ 6 | export const pick = (codec: t.ObjectCodec, keys: Keys[]) => { 7 | // Filter the shape by the specified keys 8 | const newShape = Object.fromEntries( 9 | Object.entries(codec.props.shape).filter(([key]) => keys.includes(key as Keys)) 10 | ) as Pick; 11 | 12 | // Return a new codec with the narrowed shape 13 | return t.object(newShape) as t.ObjectCodec>; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/service-errors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-errors", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "version": "0.3.6", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "license": "FSL-1.1-ALv2", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "files": [ 12 | "dist/**/*" 13 | ], 14 | "type": "module", 15 | "scripts": { 16 | "clean": "rm -r ./dist && tsc -b --clean", 17 | "build": "tsc -b", 18 | "build:tests": "tsc -b test/tsconfig.json", 19 | "test": "vitest" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": {} 23 | } 24 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/SdkReporting.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { bigint, jsonb } from '../codecs.js'; 3 | 4 | export const Sdks = t.object({ 5 | sdk: t.string, 6 | clients: t.number, 7 | users: t.number 8 | }); 9 | 10 | export type Sdks = t.Encoded; 11 | 12 | export const SdkReporting = t.object({ 13 | users: bigint, 14 | sdks: t 15 | .object({ 16 | data: jsonb(t.array(Sdks)) 17 | }) 18 | .optional() 19 | .or(t.Null) 20 | }); 21 | 22 | export type SdkReporting = t.Encoded; 23 | export type SdkReportingDecoded = t.Decoded; 24 | -------------------------------------------------------------------------------- /libs/lib-services/src/alerts/definitions.ts: -------------------------------------------------------------------------------- 1 | import * as errors from '../errors/errors-index.js'; 2 | 3 | export type Primitive = string | number | boolean; 4 | 5 | export type CaptureOptions = { 6 | level?: errors.ErrorSeverity; 7 | tags?: Record; 8 | metadata?: Record; 9 | }; 10 | 11 | export type CaptureErrorFunction = (error: any, options?: CaptureOptions) => void; 12 | export type CaptureMessageFunction = (message: string, options?: CaptureOptions) => void; 13 | 14 | export type ErrorReporter = { 15 | captureException: CaptureErrorFunction; 16 | captureMessage: CaptureMessageFunction; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/service-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../types" 14 | }, 15 | { 16 | "path": "../rsocket-router" 17 | }, 18 | { 19 | "path": "../jsonbig" 20 | }, 21 | { 22 | "path": "../jpgwire" 23 | }, 24 | { 25 | "path": "../sync-rules" 26 | }, 27 | { 28 | "path": "../../libs/lib-services" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /modules/module-mysql/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true, 9 | "typeRoots": ["./node_modules/@types", "./src/replication/zongji.d.ts"] 10 | }, 11 | "include": ["src"], 12 | "references": [ 13 | { 14 | "path": "../../packages/types" 15 | }, 16 | { 17 | "path": "../../packages/sync-rules" 18 | }, 19 | { 20 | "path": "../../packages/service-core" 21 | }, 22 | { 23 | "path": "../../libs/lib-services" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/jsonbig/src/json_container.ts: -------------------------------------------------------------------------------- 1 | import { JSONBig } from './json.js'; 2 | 3 | /** 4 | * Store a JSON text value. Used to mark the value as JSON-sourced, but only parse if really needed. 5 | */ 6 | export class JsonContainer { 7 | readonly data: string; 8 | 9 | constructor(data: string) { 10 | this.data = data; 11 | } 12 | 13 | parsed() { 14 | return JSONBig.parse(this.data); 15 | } 16 | 17 | /** 18 | * 19 | * @returns The JSON representation 20 | */ 21 | toString() { 22 | return this.data; 23 | } 24 | 25 | /** 26 | * 27 | * @returns The parsed representation 28 | */ 29 | toJSON() { 30 | return this.parsed(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/jpgwire/src/pgwire.ts: -------------------------------------------------------------------------------- 1 | // The pgwire module system doesn't play nice with node. 2 | // This works around it, and provides a proper export. 3 | 4 | // mod.js (mod.d.ts) contains the types, but the implementation is for Deno. 5 | export type * from './legacy_pgwire_types.js'; 6 | 7 | import * as pgwire_untyped from './pgwire_node.js'; 8 | import type * as pgwire_typed from './legacy_pgwire_types.js'; 9 | 10 | export const pgconnect: typeof pgwire_typed.pgconnect = (pgwire_untyped as any).pgconnect; 11 | export const pgconnection: typeof pgwire_typed.pgconnection = (pgwire_untyped as any).pgconnection; 12 | export const pgpool: typeof pgwire_typed.pgpool = (pgwire_untyped as any).pgpool; 13 | -------------------------------------------------------------------------------- /modules/module-mongodb/test/src/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | export const env = utils.collectEnvironmentVariables({ 4 | MONGO_TEST_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test'), 5 | MONGO_TEST_DATA_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test_data'), 6 | PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5431/powersync_storage_test'), 7 | CI: utils.type.boolean.default('false'), 8 | SLOW_TESTS: utils.type.boolean.default('false'), 9 | TEST_MONGO_STORAGE: utils.type.boolean.default('true'), 10 | TEST_POSTGRES_STORAGE: utils.type.boolean.default('true') 11 | }); 12 | -------------------------------------------------------------------------------- /modules/module-mysql/test/src/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | export const env = utils.collectEnvironmentVariables({ 4 | MYSQL_TEST_URI: utils.type.string.default('mysql://root:mypassword@localhost:3306/mydatabase'), 5 | MONGO_TEST_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test'), 6 | PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5431/powersync_storage_test'), 7 | CI: utils.type.boolean.default('false'), 8 | SLOW_TESTS: utils.type.boolean.default('false'), 9 | TEST_MONGO_STORAGE: utils.type.boolean.default('true'), 10 | TEST_POSTGRES_STORAGE: utils.type.boolean.default('true') 11 | }); 12 | -------------------------------------------------------------------------------- /modules/module-mssql/test/src/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | export const env = utils.collectEnvironmentVariables({ 4 | MSSQL_TEST_URI: utils.type.string.default(`mssql://sa:321strong_ROOT_password@localhost:1433/powersync`), 5 | MONGO_TEST_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test'), 6 | CI: utils.type.boolean.default('false'), 7 | SLOW_TESTS: utils.type.boolean.default('false'), 8 | PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5431/powersync_storage_test'), 9 | TEST_MONGO_STORAGE: utils.type.boolean.default('true'), 10 | TEST_POSTGRES_STORAGE: utils.type.boolean.default('true') 11 | }); 12 | -------------------------------------------------------------------------------- /modules/module-postgres/test/src/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | export const env = utils.collectEnvironmentVariables({ 4 | PG_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5432/powersync_test'), 5 | PG_STORAGE_TEST_URL: utils.type.string.default('postgres://postgres:postgres@localhost:5431/powersync_storage_test'), 6 | MONGO_TEST_URL: utils.type.string.default('mongodb://localhost:27017/powersync_test'), 7 | CI: utils.type.boolean.default('false'), 8 | SLOW_TESTS: utils.type.boolean.default('false'), 9 | TEST_MONGO_STORAGE: utils.type.boolean.default('true'), 10 | TEST_POSTGRES_STORAGE: utils.type.boolean.default('true') 11 | }); 12 | -------------------------------------------------------------------------------- /packages/schema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist", 6 | "rootDir": "src", 7 | "skipLibCheck": true 8 | }, 9 | "include": ["src"], 10 | "references": [ 11 | { 12 | "path": "../types" 13 | }, 14 | { 15 | "path": "../../modules/module-postgres" 16 | }, 17 | { 18 | "path": "../../modules/module-postgres-storage" 19 | }, 20 | { 21 | "path": "../../modules/module-mongodb" 22 | }, 23 | { 24 | "path": "../../modules/module-mongodb-storage" 25 | }, 26 | { 27 | "path": "../../modules/module-mysql" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/service-core/src/storage/storage-index.ts: -------------------------------------------------------------------------------- 1 | export * from './bson.js'; 2 | export * from './BucketStorage.js'; 3 | export * from './ChecksumCache.js'; 4 | export * from './ReplicationEventPayload.js'; 5 | export * from './SourceEntity.js'; 6 | export * from './SourceTable.js'; 7 | export * from './StorageEngine.js'; 8 | export * from './StorageProvider.js'; 9 | export * from './storage-metrics.js'; 10 | export * from './WriteCheckpointAPI.js'; 11 | export * from './BucketStorageFactory.js'; 12 | export * from './BucketStorageBatch.js'; 13 | export * from './SyncRulesBucketStorage.js'; 14 | export * from './PersistedSyncRulesContent.js'; 15 | export * from './ReplicationLock.js'; 16 | export * from './ReportStorage.js'; 17 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../../packages/types" 14 | }, 15 | { 16 | "path": "../../packages/jsonbig" 17 | }, 18 | { 19 | "path": "../../packages/sync-rules" 20 | }, 21 | { 22 | "path": "../../packages/service-core" 23 | }, 24 | { 25 | "path": "../../libs/lib-services" 26 | }, 27 | { 28 | "path": "../../libs/lib-mongodb" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/service-core-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../types" 14 | }, 15 | { 16 | "path": "../rsocket-router" 17 | }, 18 | { 19 | "path": "../jsonbig" 20 | }, 21 | { 22 | "path": "../jpgwire" 23 | }, 24 | { 25 | "path": "../sync-rules" 26 | }, 27 | { 28 | "path": "../service-core" 29 | }, 30 | { 31 | "path": "../../libs/lib-services" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/utils/bson.ts: -------------------------------------------------------------------------------- 1 | import { storage, utils } from '@powersync/service-core'; 2 | import * as uuid from 'uuid'; 3 | 4 | /** 5 | * BSON is used to serialize certain documents for storage in BYTEA columns. 6 | * JSONB columns do not directly support storing binary data which could be required in future. 7 | */ 8 | 9 | export function replicaIdToSubkey(tableId: string, id: storage.ReplicaId): string { 10 | // Hashed UUID from the table and id 11 | if (storage.isUUID(id)) { 12 | // Special case for UUID for backwards-compatiblity 13 | return `${tableId}/${id.toHexString()}`; 14 | } 15 | const repr = storage.serializeBson({ table: tableId, id }); 16 | return uuid.v5(repr, utils.ID_NAMESPACE); 17 | } 18 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/sync-rules/impl/inline-sync-rules-collector.ts: -------------------------------------------------------------------------------- 1 | import { SyncRulesConfig } from '../../types.js'; 2 | import { SyncRulesCollector } from '../sync-collector.js'; 3 | import { configFile } from '@powersync/service-types'; 4 | 5 | export class InlineSyncRulesCollector extends SyncRulesCollector { 6 | get name(): string { 7 | return 'Inline'; 8 | } 9 | 10 | async collect(baseConfig: configFile.PowerSyncConfig): Promise { 11 | const content = baseConfig.sync_rules?.content; 12 | if (!content) { 13 | return null; 14 | } 15 | 16 | return { 17 | present: true, 18 | exit_on_error: true, 19 | ...baseConfig.sync_rules 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/types/types.ts: -------------------------------------------------------------------------------- 1 | import * as lib_mongo from '@powersync/lib-service-mongodb'; 2 | import * as service_types from '@powersync/service-types'; 3 | import * as t from 'ts-codec'; 4 | 5 | export const MongoStorageConfig = lib_mongo.BaseMongoConfig.and( 6 | t.object({ 7 | // Add any mongo specific storage settings here in future 8 | }) 9 | ); 10 | 11 | export type MongoStorageConfig = t.Encoded; 12 | export type MongoStorageConfigDecoded = t.Decoded; 13 | 14 | export function isMongoStorageConfig( 15 | config: service_types.configFile.GenericStorageConfig 16 | ): config is MongoStorageConfig { 17 | return config.type == lib_mongo.MONGO_CONNECTION_TYPE; 18 | } 19 | -------------------------------------------------------------------------------- /modules/module-mssql/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../../../packages/service-core/src/*"], 12 | "@module/*": ["../src/*"], 13 | "@core-tests/*": ["../../../packages/service-core/test/src/*"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { 19 | "path": "../" 20 | }, 21 | { 22 | "path": "../../../packages/service-core/test" 23 | }, 24 | { 25 | "path": "../../../packages/service-core/" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /modules/module-mysql/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../../../packages/service-core/src/*"], 12 | "@module/*": ["../src/*"], 13 | "@core-tests/*": ["../../../packages/service-core/test/src/*"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { 19 | "path": "../" 20 | }, 21 | { 22 | "path": "../../../packages/service-core/test" 23 | }, 24 | { 25 | "path": "../../../packages/service-core/" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /modules/module-postgres/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../../../packages/service-core/src/*"], 12 | "@module/*": ["../src/*"], 13 | "@core-tests/*": ["../../../packages/service-core/test/src/*"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { 19 | "path": "../" 20 | }, 21 | { 22 | "path": "../../../packages/service-core/test" 23 | }, 24 | { 25 | "path": "../../../packages/service-core/" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/storage/implementation/MongoIdSequence.ts: -------------------------------------------------------------------------------- 1 | import { ReplicationAssertionError } from '@powersync/lib-services-framework'; 2 | 3 | /** 4 | * Manages op_id or similar sequence in memory. 5 | * 6 | * This is typically used within a transaction, with the last value persisted 7 | * at the end of the transaction. 8 | */ 9 | export class MongoIdSequence { 10 | private _last: bigint; 11 | 12 | constructor(last: bigint) { 13 | if (typeof last != 'bigint') { 14 | throw new ReplicationAssertionError(`BigInt required, got ${last} ${typeof last}`); 15 | } 16 | this._last = last; 17 | } 18 | 19 | next() { 20 | return ++this._last; 21 | } 22 | 23 | last() { 24 | return this._last; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/service-core/src/auth/StaticKeyCollector.ts: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose'; 2 | import { KeySpec } from './KeySpec.js'; 3 | import { KeyCollector, KeyResult } from './KeyCollector.js'; 4 | 5 | /** 6 | * Set of static keys. 7 | * 8 | * A key can be added both with and without a kid, in case wildcard matching is desired. 9 | */ 10 | export class StaticKeyCollector implements KeyCollector { 11 | static async importKeys(keys: jose.JWK[]) { 12 | const parsedKeys = await Promise.all(keys.map((key) => KeySpec.importKey(key))); 13 | return new StaticKeyCollector(parsedKeys); 14 | } 15 | 16 | constructor(private keys: KeySpec[]) {} 17 | 18 | async getKeys(): Promise { 19 | return { keys: this.keys, errors: [] }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/sync-rules/sync-rules-provider.ts: -------------------------------------------------------------------------------- 1 | import { SyncRulesConfig } from '../types.js'; 2 | import fs from 'fs/promises'; 3 | 4 | export interface SyncRulesProvider { 5 | get(): Promise; 6 | 7 | readonly exitOnError: boolean; 8 | } 9 | 10 | export class ConfigurationFileSyncRulesProvider implements SyncRulesProvider { 11 | constructor(private config: SyncRulesConfig) {} 12 | 13 | async get(): Promise { 14 | if (this.config.content) { 15 | return this.config.content; 16 | } else if (this.config.path) { 17 | return await fs.readFile(this.config.path, 'utf-8'); 18 | } 19 | } 20 | 21 | get exitOnError() { 22 | return this.config.exit_on_error; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/lib-services/src/schema/json-schema/keywords.ts: -------------------------------------------------------------------------------- 1 | import * as ajv from 'ajv'; 2 | 3 | export const BufferNodeType: ajv.KeywordDefinition = { 4 | keyword: 'nodeType', 5 | metaSchema: { 6 | type: 'string', 7 | enum: ['buffer', 'date'] 8 | }, 9 | error: { 10 | message: ({ schemaCode }) => { 11 | return ajv.str`should be a ${schemaCode}`; 12 | } 13 | }, 14 | code(context) { 15 | switch (context.schema) { 16 | case 'buffer': { 17 | return context.fail(ajv._`!Buffer.isBuffer(${context.data})`); 18 | } 19 | case 'date': { 20 | return context.fail(ajv._`!(${context.data} instanceof Date)`); 21 | } 22 | default: { 23 | context.fail(ajv._`true`); 24 | } 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/test/src/__snapshots__/storage.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`Mongo Sync Bucket Storage - Data > empty storage metrics 1`] = ` 4 | { 5 | "operations_size_bytes": 0, 6 | "parameters_size_bytes": 0, 7 | "replication_size_bytes": 0, 8 | } 9 | `; 10 | 11 | exports[`Mongo Sync Bucket Storage - split buckets > empty storage metrics 1`] = ` 12 | { 13 | "operations_size_bytes": 0, 14 | "parameters_size_bytes": 0, 15 | "replication_size_bytes": 0, 16 | } 17 | `; 18 | 19 | exports[`Mongo Sync Bucket Storage - split operations > empty storage metrics 1`] = ` 20 | { 21 | "operations_size_bytes": 0, 22 | "parameters_size_bytes": 0, 23 | "replication_size_bytes": 0, 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/WriteCheckpoint.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { bigint, jsonb } from '../codecs.js'; 3 | 4 | export const WriteCheckpoint = t.object({ 5 | user_id: t.string, 6 | lsns: jsonb(t.record(t.string)), 7 | write_checkpoint: bigint 8 | }); 9 | 10 | export type WriteCheckpoint = t.Encoded; 11 | export type WriteCheckpointDecoded = t.Decoded; 12 | 13 | export const CustomWriteCheckpoint = t.object({ 14 | user_id: t.string, 15 | write_checkpoint: bigint, 16 | sync_rules_id: bigint 17 | }); 18 | 19 | export type CustomWriteCheckpoint = t.Encoded; 20 | export type CustomWriteCheckpointDecoded = t.Decoded; 21 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/ActiveCheckpointNotification.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { jsonb } from '../codecs.js'; 3 | import { ActiveCheckpoint } from './ActiveCheckpoint.js'; 4 | 5 | export const ActiveCheckpointPayload = t.object({ 6 | active_checkpoint: ActiveCheckpoint 7 | }); 8 | 9 | export type ActiveCheckpointPayload = t.Encoded; 10 | export type ActiveCheckpointPayloadDecoded = t.Decoded; 11 | 12 | export const ActiveCheckpointNotification = jsonb(ActiveCheckpointPayload); 13 | export type ActiveCheckpointNotification = t.Encoded; 14 | export type ActiveCheckpointNotificationDecoded = t.Decoded; 15 | -------------------------------------------------------------------------------- /packages/sync-rules/src/schema-generators/schema-generators.ts: -------------------------------------------------------------------------------- 1 | import { SchemaGenerator } from './SchemaGenerator.js'; 2 | import { SqlSchemaGenerator } from './SqlSchemaGenerator.js'; 3 | 4 | export * from './DartSchemaGenerator.js'; 5 | export * from './DotNetSchemaGenerator.js'; 6 | export * from './generators.js'; 7 | export * from './JsLegacySchemaGenerator.js'; 8 | export * from './KotlinSchemaGenerator.js'; 9 | export * from './RoomSchemaGenerator.js'; 10 | export * from './SchemaGenerator.js'; 11 | export * from './SwiftSchemaGenerator.js'; 12 | export * from './TsSchemaGenerator.js'; 13 | 14 | export const driftSchemaGenerator = new SqlSchemaGenerator('Drift', 'tables.drift'); 15 | export const sqlDelightSchemaGenerator = new SqlSchemaGenerator('SQLDelight', 'tables.sq'); 16 | -------------------------------------------------------------------------------- /modules/module-mongodb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../../packages/types" 14 | }, 15 | { 16 | "path": "../../packages/jsonbig" 17 | }, 18 | { 19 | "path": "../../packages/sync-rules" 20 | }, 21 | { 22 | "path": "../../packages/service-core" 23 | }, 24 | { 25 | "path": "../../libs/lib-services" 26 | }, 27 | { 28 | "path": "../../libs/lib-mongodb" 29 | }, 30 | { 31 | "path": "../module-mongodb-storage" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /modules/module-postgres/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../../packages/types" 14 | }, 15 | { 16 | "path": "../../packages/jsonbig" 17 | }, 18 | { 19 | "path": "../../packages/jpgwire" 20 | }, 21 | { 22 | "path": "../../packages/sync-rules" 23 | }, 24 | { 25 | "path": "../../packages/service-core" 26 | }, 27 | { 28 | "path": "../../libs/lib-services" 29 | }, 30 | { 31 | "path": "../../libs/lib-postgres" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /packages/rsocket-router/src/utils/BaseObserver.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | 3 | export class BaseObserver { 4 | protected listeners: { [id: string]: Partial }; 5 | 6 | constructor() { 7 | this.listeners = {}; 8 | } 9 | 10 | registerListener(listener: Partial): () => void { 11 | const id = uuid(); 12 | this.listeners[id] = listener; 13 | return () => { 14 | delete this.listeners[id]; 15 | }; 16 | } 17 | 18 | iterateListeners(cb: (listener: Partial) => any) { 19 | for (let i in this.listeners) { 20 | cb(this.listeners[i]); 21 | } 22 | } 23 | 24 | async iterateAsyncListeners(cb: (listener: Partial) => Promise) { 25 | for (let i in this.listeners) { 26 | await cb(this.listeners[i]); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/CurrentData.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { hexBuffer, jsonb, pgwire_number } from '../codecs.js'; 3 | 4 | export const CurrentBucket = t.object({ 5 | bucket: t.string, 6 | table: t.string, 7 | id: t.string 8 | }); 9 | 10 | export type CurrentBucket = t.Encoded; 11 | export type CurrentBucketDecoded = t.Decoded; 12 | 13 | export const CurrentData = t.object({ 14 | buckets: jsonb(t.array(CurrentBucket)), 15 | data: hexBuffer, 16 | group_id: pgwire_number, 17 | lookups: t.array(hexBuffer), 18 | source_key: hexBuffer, 19 | source_table: t.string 20 | }); 21 | 22 | export type CurrentData = t.Encoded; 23 | export type CurrentDataDecoded = t.Decoded; 24 | -------------------------------------------------------------------------------- /packages/service-core/src/api/schema.ts: -------------------------------------------------------------------------------- 1 | import { internal_routes } from '@powersync/service-types'; 2 | 3 | import * as api from '../api/api-index.js'; 4 | 5 | export async function getConnectionsSchema(api: api.RouteAPI): Promise { 6 | if (!api) { 7 | return { 8 | connections: [], 9 | defaultConnectionTag: 'default', 10 | defaultSchema: '' 11 | }; 12 | } 13 | 14 | const baseConfig = await api.getSourceConfig(); 15 | 16 | return { 17 | connections: [ 18 | { 19 | id: baseConfig.id, 20 | tag: baseConfig.tag, 21 | schemas: await api.getConnectionSchema() 22 | } 23 | ], 24 | defaultConnectionTag: baseConfig.tag!, 25 | defaultSchema: api.getParseSyncRulesOptions().defaultSchema 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /modules/module-mysql/test/src/mysql-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { isVersionAtLeast } from '@module/utils/mysql-utils.js'; 3 | 4 | describe('MySQL Utility Tests', () => { 5 | test('Minimum version checking ', () => { 6 | const newerVersion = '8.4.0'; 7 | const olderVersion = '5.7'; 8 | const sameVersion = '8.0'; 9 | // Improperly formatted semantic versions should be handled gracefully if possible 10 | const improperSemver = '5.7.42-0ubuntu0.18.04.1-log'; 11 | 12 | expect(isVersionAtLeast(newerVersion, '8.0')).toBeTruthy(); 13 | expect(isVersionAtLeast(sameVersion, '8.0')).toBeTruthy(); 14 | expect(isVersionAtLeast(olderVersion, '8.0')).toBeFalsy(); 15 | expect(isVersionAtLeast(improperSemver, '5.7')).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/jsonbig/README.md: -------------------------------------------------------------------------------- 1 | # powersync-jsonbig 2 | 3 | JSON is used everywhere, including: 4 | 5 | 1. PostgreSQL (json/jsonb types) 6 | 2. Sync rules input (values are normalized to JSON text). 7 | 3. Sync rule transformations (extracting values, constructing objects in the future) 8 | 4. Persisting data in the database. 9 | 5. Sending to the client. 10 | 11 | Where we can, JSON data is kept as strings and not parsed. 12 | This is so that: 13 | 14 | 1. We don't add parsing / serializing overhead. 15 | 2. We don't change the data. 16 | 17 | Specifically: 18 | 19 | 1. The SQLite type system makes a distinction between INTEGER and REAL values. We try to preserve this. 20 | 2. Integers in SQLite can be up to 64-bit. 21 | 22 | For this, we use a custom parser, and use BigInt for all integers, and number for floats. 23 | -------------------------------------------------------------------------------- /packages/schema/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @powersync/service-schema 2 | 3 | ## 1.18.1 4 | 5 | ## 1.18.0 6 | 7 | ## 1.17.0 8 | 9 | ## 1.16.3 10 | 11 | ## 1.16.2 12 | 13 | ## 1.16.1 14 | 15 | ## 1.16.0 16 | 17 | ### Patch Changes 18 | 19 | - 88982d9: Migrate to trusted publishing 20 | 21 | ## 1.15.8 22 | 23 | ## 1.15.7 24 | 25 | ## 1.15.6 26 | 27 | ## 1.15.5 28 | 29 | ## 1.15.4 30 | 31 | ## 1.15.3 32 | 33 | ## 1.15.2 34 | 35 | ## 1.15.1 36 | 37 | ## 1.15.0 38 | 39 | ### Patch Changes 40 | 41 | - 060b829: Update license abbreviation to FSL-1.1-ALv2. 42 | 43 | ## 1.14.0 44 | 45 | ## 1.13.4 46 | 47 | ## 1.13.3 48 | 49 | ## 1.13.2 50 | 51 | ## 1.13.1 52 | 53 | ## 1.13.0 54 | 55 | ## 1.12.1 56 | 57 | ## 1.12.0 58 | 59 | ### Minor Changes 60 | 61 | - c672380: Added JSON schema export for base PowerSyncConfig 62 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/collectors/impl/fallback-config-collector.ts: -------------------------------------------------------------------------------- 1 | import { RunnerConfig } from '../../types.js'; 2 | import { FileSystemConfigCollector } from './filesystem-config-collector.js'; 3 | 4 | export const DEFAULT_CONFIG_LOCATION = 'powersync.yaml'; 5 | 6 | /** 7 | * Falls back to reading the PowerSync config file from the previous 8 | * default location of `powersync.yaml` 9 | */ 10 | export class FallbackConfigCollector extends FileSystemConfigCollector { 11 | get name(): string { 12 | return 'Fallback powersync.yaml'; 13 | } 14 | 15 | async collectSerialized(runnerConfig: RunnerConfig) { 16 | return super.collectSerialized({ 17 | ...runnerConfig, 18 | // Use the fallback config location 19 | config_path: DEFAULT_CONFIG_LOCATION 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/BucketData.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { bigint, hexBuffer, pgwire_number } from '../codecs.js'; 3 | 4 | export enum OpType { 5 | PUT = 'PUT', 6 | REMOVE = 'REMOVE', 7 | MOVE = 'MOVE', 8 | CLEAR = 'CLEAR' 9 | } 10 | 11 | export const BucketData = t.object({ 12 | group_id: pgwire_number, 13 | bucket_name: t.string, 14 | op_id: bigint, 15 | op: t.Enum(OpType), 16 | source_table: t.Null.or(t.string), 17 | source_key: t.Null.or(hexBuffer), 18 | table_name: t.string.or(t.Null), 19 | row_id: t.string.or(t.Null), 20 | checksum: bigint, 21 | data: t.Null.or(t.string), 22 | target_op: t.Null.or(bigint) 23 | }); 24 | 25 | export type BucketData = t.Encoded; 26 | export type BucketDataDecoded = t.Decoded; 27 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declarationDir": "dist/@types", 6 | "tsBuildInfoFile": "dist/.tsbuildinfo", 7 | "rootDir": "src", 8 | "skipLibCheck": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../../packages/types" 14 | }, 15 | { 16 | "path": "../../packages/jsonbig" 17 | }, 18 | { 19 | "path": "../../packages/jpgwire" 20 | }, 21 | { 22 | "path": "../../packages/sync-rules" 23 | }, 24 | { 25 | "path": "../../packages/service-core" 26 | }, 27 | { 28 | "path": "../../libs/lib-services" 29 | }, 30 | { 31 | "path": "../../libs/lib-postgres" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /modules/module-mongodb/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../../../packages/service-core/src/*"], 12 | "@module/*": ["../src/*"], 13 | "@core-tests/*": ["../../../packages/service-core/test/src/*"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { 19 | "path": "../" 20 | }, 21 | { 22 | "path": "../../../packages/service-core/test" 23 | }, 24 | { 25 | "path": "../../../packages/service-core/" 26 | }, 27 | { 28 | "path": "../../../packages/service-core-tests/" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "baseUrl": "./", 6 | "noEmit": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "paths": { 11 | "@/*": ["../../../packages/service-core/src/*"], 12 | "@module/*": ["../src/*"], 13 | "@core-tests/*": ["../../../packages/service-core/test/src/*"] 14 | } 15 | }, 16 | "include": ["src"], 17 | "references": [ 18 | { 19 | "path": "../" 20 | }, 21 | { 22 | "path": "../../../packages/service-core/test" 23 | }, 24 | { 25 | "path": "../../../packages/service-core/" 26 | }, 27 | { 28 | "path": "../../../packages/service-core-tests/" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/service-core/src/replication/RelationCache.ts: -------------------------------------------------------------------------------- 1 | import { SourceTable } from '../storage/SourceTable.js'; 2 | 3 | export class RelationCache { 4 | private cache = new Map(); 5 | private idFunction: (item: T | SourceTable) => string | number; 6 | 7 | constructor(idFunction: (item: T | SourceTable) => string | number) { 8 | this.idFunction = idFunction; 9 | } 10 | 11 | update(table: SourceTable) { 12 | const id = this.idFunction(table); 13 | this.cache.set(id, table); 14 | } 15 | 16 | get(source: T | SourceTable): SourceTable | undefined { 17 | const id = this.idFunction(source); 18 | return this.cache.get(id); 19 | } 20 | 21 | delete(source: T | SourceTable): boolean { 22 | const id = this.idFunction(source); 23 | return this.cache.delete(id); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /modules/module-mssql/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @powersync/service-module-mssql 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - 21b3a41: Fixed sync rule validation query for mssql 8 | - Updated dependencies [21b3a41] 9 | - @powersync/service-sync-rules@0.29.9 10 | - @powersync/lib-services-framework@0.7.13 11 | - @powersync/service-core@1.18.1 12 | 13 | ## 0.1.0 14 | 15 | ### Minor Changes 16 | 17 | - b77bb2c: - First iteration of MSSQL replication using Change Data Capture (CDC). 18 | - Supports resumable snapshot replication 19 | - Uses CDC polling for replication 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [dc696b1] 24 | - Updated dependencies [b77bb2c] 25 | - @powersync/service-core@1.18.0 26 | - @powersync/service-types@0.13.3 27 | - @powersync/service-errors@0.3.6 28 | - @powersync/lib-services-framework@0.7.12 29 | -------------------------------------------------------------------------------- /packages/service-core/src/util/env.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/lib-services-framework'; 2 | 3 | import { ServiceRunner } from './config/types.js'; 4 | 5 | export const env = utils.collectEnvironmentVariables({ 6 | /** 7 | * Path to configuration file in filesystem 8 | */ 9 | POWERSYNC_CONFIG_PATH: utils.type.string.optional(), 10 | /** 11 | * Base64 encoded contents of configuration file 12 | */ 13 | POWERSYNC_CONFIG_B64: utils.type.string.optional(), 14 | /** 15 | * Base64 encoded contents of sync rules YAML 16 | */ 17 | POWERSYNC_SYNC_RULES_B64: utils.type.string.optional(), 18 | /** 19 | * Runner to be started in this process 20 | */ 21 | PS_RUNNER_TYPE: utils.type.string.default(ServiceRunner.UNIFIED), 22 | 23 | NODE_ENV: utils.type.string.optional() 24 | }); 25 | 26 | export type Env = typeof env; 27 | -------------------------------------------------------------------------------- /packages/service-errors/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { ErrorData, InternalServerError, ServiceError } from './errors.js'; 2 | 3 | export const isServiceError = (err: any): err is ServiceError => { 4 | const matches = ServiceError.isServiceError(err); 5 | return !!matches; 6 | }; 7 | 8 | export const asServiceError = (err: any): ServiceError => { 9 | if (ServiceError.isServiceError(err)) { 10 | return err; 11 | } else { 12 | return new InternalServerError(err); 13 | } 14 | }; 15 | 16 | export const getErrorData = (err: Error | any): ErrorData | undefined => { 17 | if (!isServiceError(err)) { 18 | return; 19 | } 20 | return err.toJSON(); 21 | }; 22 | 23 | export const matchesErrorCode = (err: Error | any, code: string) => { 24 | if (isServiceError(err)) { 25 | return err.errorData.code === code; 26 | } 27 | return false; 28 | }; 29 | -------------------------------------------------------------------------------- /modules/module-mysql/src/types/node-sql-parser-extended-types.ts: -------------------------------------------------------------------------------- 1 | import 'node-sql-parser'; 2 | 3 | /** 4 | * Missing Type definitions for the node-sql-parser 5 | */ 6 | declare module 'node-sql-parser' { 7 | interface RenameStatement { 8 | type: 'rename'; 9 | table: { db: string | null; table: string }[][]; 10 | } 11 | 12 | interface TruncateStatement { 13 | type: 'truncate'; 14 | keyword: 'table'; // There are more keywords possible, but we only care about 'table' 15 | name: { db: string | null; table: string; as: string | null }[]; 16 | } 17 | 18 | // This custom type more accurately describes what the structure of a Drop statement looks like for indexes. 19 | interface DropIndexStatement { 20 | type: 'drop'; 21 | keyword: 'index'; 22 | table: { db: string | null; table: string }; 23 | name: any[]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/sync-rules/impl/base64-sync-rules-collector.ts: -------------------------------------------------------------------------------- 1 | import { RunnerConfig, SyncRulesConfig } from '../../types.js'; 2 | import { SyncRulesCollector } from '../sync-collector.js'; 3 | import { configFile } from '@powersync/service-types'; 4 | 5 | export class Base64SyncRulesCollector extends SyncRulesCollector { 6 | get name(): string { 7 | return 'Base64'; 8 | } 9 | 10 | async collect(baseConfig: configFile.PowerSyncConfig, runnerConfig: RunnerConfig): Promise { 11 | const { sync_rules_base64 } = runnerConfig; 12 | if (!sync_rules_base64) { 13 | return null; 14 | } 15 | 16 | return { 17 | present: true, 18 | exit_on_error: baseConfig.sync_rules?.exit_on_error ?? true, 19 | content: Buffer.from(sync_rules_base64, 'base64').toString() 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/storage/storage-index.ts: -------------------------------------------------------------------------------- 1 | export * from './implementation/db.js'; 2 | export * from './implementation/models.js'; 3 | export * from './implementation/MongoBucketBatch.js'; 4 | export * from './implementation/MongoIdSequence.js'; 5 | export * from './implementation/MongoPersistedSyncRules.js'; 6 | export * from './implementation/MongoPersistedSyncRulesContent.js'; 7 | export * from './implementation/MongoStorageProvider.js'; 8 | export * from './implementation/MongoSyncBucketStorage.js'; 9 | export * from './implementation/MongoSyncRulesLock.js'; 10 | export * from './implementation/OperationBatch.js'; 11 | export * from './implementation/PersistedBatch.js'; 12 | export * from '../utils/util.js'; 13 | export * from './MongoBucketStorage.js'; 14 | export * from './MongoReportStorage.js'; 15 | export * as test_utils from '../utils/test-utils.js'; 16 | -------------------------------------------------------------------------------- /packages/service-core/src/storage/SourceEntity.ts: -------------------------------------------------------------------------------- 1 | export interface ColumnDescriptor { 2 | name: string; 3 | /** 4 | * The type of the column ie VARCHAR, INT, etc 5 | */ 6 | type?: string; 7 | /** 8 | * Some data sources have a type id that can be used to identify the type of the column 9 | */ 10 | typeId?: number; 11 | } 12 | 13 | export interface SourceEntityDescriptor { 14 | /** 15 | * The internal id of the source entity structure in the database. 16 | * If undefined, the schema and name are used as the identifier. 17 | * If specified, this is specifically used to detect renames. 18 | */ 19 | objectId: number | string | undefined; 20 | schema: string; 21 | name: string; 22 | /** 23 | * The columns that are used to uniquely identify a record in the source entity. 24 | */ 25 | replicaIdColumns: ColumnDescriptor[]; 26 | } 27 | -------------------------------------------------------------------------------- /packages/jpgwire/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-jpgwire", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "types": "dist/index.d.ts", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "version": "0.21.8", 9 | "main": "dist/index.js", 10 | "license": "FSL-1.1-ALv2", 11 | "files": [ 12 | "dist/**/*", 13 | "ca/*.pem" 14 | ], 15 | "type": "module", 16 | "scripts": { 17 | "clean": "rm -r ./dist && tsc -b --clean", 18 | "build": "tsc -b", 19 | "build:tests": "tsc -b test/tsconfig.json", 20 | "test": "vitest" 21 | }, 22 | "dependencies": { 23 | "@powersync/service-jsonbig": "workspace:^", 24 | "@powersync/service-sync-rules": "workspace:^", 25 | "date-fns": "^4.1.0", 26 | "pgwire": "^0.8.1" 27 | }, 28 | "devDependencies": { 29 | "vitest": "^3.2.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/service-core-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-core-tests", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "types": "dist/index.d.ts", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "version": "0.12.15", 9 | "main": "dist/index.js", 10 | "license": "FSL-1.1-ALv2", 11 | "type": "module", 12 | "scripts": { 13 | "build": "tsc -b", 14 | "clean": "rm -rf ./dist && tsc -b --clean" 15 | }, 16 | "dependencies": { 17 | "@powersync/service-core": "workspace:^", 18 | "@powersync/service-jsonbig": "workspace:^", 19 | "@powersync/service-sync-rules": "workspace:^" 20 | }, 21 | "peerDependencies": { 22 | "vite-tsconfig-paths": "^5.1.4", 23 | "vitest": "^3.2.4" 24 | }, 25 | "devDependencies": { 26 | "typescript": "^5.7.3", 27 | "@opentelemetry/sdk-metrics": "^1.30.1" 28 | } 29 | } -------------------------------------------------------------------------------- /packages/service-core/src/util/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises'; 2 | 3 | import { container } from '@powersync/lib-services-framework'; 4 | import { ResolvedPowerSyncConfig, RunnerConfig } from './config/types.js'; 5 | import { CompoundConfigCollector } from './util-index.js'; 6 | 7 | /** 8 | * Loads the resolved config using the registered config collector 9 | */ 10 | export async function loadConfig(runnerConfig: RunnerConfig) { 11 | const collector = container.getImplementation(CompoundConfigCollector); 12 | return collector.collectConfig(runnerConfig); 13 | } 14 | 15 | export async function loadSyncRules(config: ResolvedPowerSyncConfig): Promise { 16 | const sync_rules = config.sync_rules; 17 | if (sync_rules.content) { 18 | return sync_rules.content; 19 | } else if (sync_rules.path) { 20 | return await fs.readFile(sync_rules.path, 'utf-8'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /service/src/util/modules.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@powersync/service-core'; 2 | 3 | export const DYNAMIC_MODULES: core.ModuleLoaders = { 4 | connection: { 5 | mongodb: () => import('@powersync/service-module-mongodb').then((module) => new module.MongoModule()), 6 | mysql: () => import('@powersync/service-module-mysql').then((module) => new module.MySQLModule()), 7 | mssql: () => import('@powersync/service-module-mssql').then((module) => new module.MSSQLModule()), 8 | postgresql: () => import('@powersync/service-module-postgres').then((module) => new module.PostgresModule()) 9 | }, 10 | storage: { 11 | mongodb: () => 12 | import('@powersync/service-module-mongodb-storage').then((module) => new module.MongoStorageModule()), 13 | postgresql: () => 14 | import('@powersync/service-module-postgres-storage').then((module) => new module.PostgresStorageModule()) 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /packages/rsocket-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-rsocket-router", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "version": "0.2.10", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "license": "FSL-1.1-ALv2", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "files": [ 12 | "dist/**/*" 13 | ], 14 | "type": "module", 15 | "scripts": { 16 | "clean": "rm -r ./dist && tsc -b --clean", 17 | "build": "tsc -b", 18 | "test": "vitest" 19 | }, 20 | "dependencies": { 21 | "@powersync/lib-services-framework": "workspace:*", 22 | "rsocket-core": "1.0.0-alpha.3", 23 | "ts-codec": "^1.3.0", 24 | "uuid": "^11.1.0", 25 | "ws": "^8.17.0" 26 | }, 27 | "devDependencies": { 28 | "@types/ws": "~8.2.0", 29 | "bson": "^6.10.4", 30 | "rsocket-websocket-client": "1.0.0-alpha.3" 31 | } 32 | } -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/migrations/db/migrations/1752661449910-connection-reporting.ts: -------------------------------------------------------------------------------- 1 | import { migrations } from '@powersync/service-core'; 2 | import * as storage from '../../../storage/storage-index.js'; 3 | import { MongoStorageConfig } from '../../../types/types.js'; 4 | 5 | export const up: migrations.PowerSyncMigrationFunction = async (context) => { 6 | const { 7 | service_context: { configuration } 8 | } = context; 9 | // No-op - moved to 1762790715147-connection-reporting2 10 | }; 11 | 12 | export const down: migrations.PowerSyncMigrationFunction = async (context) => { 13 | const { 14 | service_context: { configuration } 15 | } = context; 16 | 17 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 18 | 19 | try { 20 | await db.db.dropCollection('connection_report_events'); 21 | } finally { 22 | await db.client.close(); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/jpgwire/src/pgwire_node.js: -------------------------------------------------------------------------------- 1 | import { pbkdf2 as _pbkdf2 } from 'crypto'; 2 | 3 | import { _net } from 'pgwire'; 4 | import { SocketAdapter } from './socket_adapter.js'; 5 | 6 | Object.assign(_net, { 7 | connectTcp(options) { 8 | return SocketAdapter.connect(options); 9 | }, 10 | connectUnix(_options) { 11 | throw `Unused and unsupported in PowerSync`; 12 | }, 13 | reconnectable(err) { 14 | return ['ENOTFOUND', 'ECONNREFUSED', 'ECONNRESET'].includes(err?.code); 15 | }, 16 | startTls(sockadapt, { hostname, caCerts }) { 17 | return sockadapt.startTls(hostname, caCerts); 18 | }, 19 | read(sockadapt, out) { 20 | return sockadapt.read(out); 21 | }, 22 | write(sockadapt, data) { 23 | return sockadapt.write(data); 24 | }, 25 | closeNullable(sockadapt) { 26 | if (!sockadapt) return; 27 | return sockadapt.close(); 28 | } 29 | }); 30 | 31 | export * from 'pgwire'; 32 | -------------------------------------------------------------------------------- /libs/lib-services/src/logger/Logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston'; 2 | 3 | const prefixFormat = winston.format((info) => { 4 | if (info.prefix) { 5 | info.message = `${info.prefix}${info.message}`; 6 | } 7 | return { 8 | ...info, 9 | prefix: undefined 10 | }; 11 | }); 12 | 13 | export namespace LogFormat { 14 | export const development = winston.format.combine( 15 | prefixFormat(), 16 | winston.format.colorize({ level: true }), 17 | winston.format.simple() 18 | ); 19 | export const production = winston.format.combine(prefixFormat(), winston.format.timestamp(), winston.format.json()); 20 | } 21 | 22 | export const logger = winston.createLogger(); 23 | 24 | // Configure logging to console as the default 25 | logger.configure({ 26 | format: process.env.NODE_ENV == 'production' ? LogFormat.production : LogFormat.development, 27 | transports: [new winston.transports.Console()] 28 | }); 29 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/utils/bucket-data.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/service-core'; 2 | import { models } from '../types/types.js'; 3 | import { replicaIdToSubkey } from './bson.js'; 4 | 5 | export const mapOpEntry = (entry: models.BucketDataDecoded) => { 6 | if (entry.op == models.OpType.PUT || entry.op == models.OpType.REMOVE) { 7 | return { 8 | op_id: utils.internalToExternalOpId(entry.op_id), 9 | op: entry.op, 10 | object_type: entry.table_name ?? undefined, 11 | object_id: entry.row_id ?? undefined, 12 | checksum: Number(entry.checksum), 13 | subkey: replicaIdToSubkey(entry.source_table!, entry.source_key!), 14 | data: entry.data 15 | }; 16 | } else { 17 | // MOVE, CLEAR 18 | 19 | return { 20 | op_id: utils.internalToExternalOpId(entry.op_id), 21 | op: entry.op, 22 | checksum: Number(entry.checksum) 23 | }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /packages/sync-rules/src/schema-generators/generators.ts: -------------------------------------------------------------------------------- 1 | import { DartFlutterFlowSchemaGenerator, DartSchemaGenerator } from './DartSchemaGenerator.js'; 2 | import { DotNetSchemaGenerator } from './DotNetSchemaGenerator.js'; 3 | import { JsLegacySchemaGenerator } from './JsLegacySchemaGenerator.js'; 4 | import { KotlinSchemaGenerator } from './KotlinSchemaGenerator.js'; 5 | import { SwiftSchemaGenerator } from './SwiftSchemaGenerator.js'; 6 | import { TsSchemaGenerator, TsSchemaLanguage } from './TsSchemaGenerator.js'; 7 | 8 | export const schemaGenerators = { 9 | dart: new DartSchemaGenerator(), 10 | dotNet: new DotNetSchemaGenerator(), 11 | flutterFlow: new DartFlutterFlowSchemaGenerator(), 12 | js: new TsSchemaGenerator({ language: TsSchemaLanguage.js }), 13 | jsLegacy: new JsLegacySchemaGenerator(), 14 | kotlin: new KotlinSchemaGenerator(), 15 | swift: new SwiftSchemaGenerator(), 16 | ts: new TsSchemaGenerator() 17 | }; 18 | -------------------------------------------------------------------------------- /packages/service-core/src/auth/KeyCollector.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizationError } from '@powersync/lib-services-framework'; 2 | import { KeySpec } from './KeySpec.js'; 3 | 4 | export interface KeyCollector { 5 | /** 6 | * Fetch keys for this collector. 7 | * 8 | * If a partial result is available, return keys and errors array. 9 | * These errors are not retried, and previous keys not cached. 10 | * 11 | * If the request fails completely, throw an error. These errors are retried. 12 | * In that case, previous keys may be cached and used. 13 | */ 14 | getKeys(): Promise; 15 | 16 | /** 17 | * Indicates that no matching key was found. 18 | * 19 | * The collector may use this as a hint to reload keys, although this is not a requirement. 20 | */ 21 | noKeyFound?: () => Promise; 22 | } 23 | 24 | export interface KeyResult { 25 | errors: AuthorizationError[]; 26 | keys: KeySpec[]; 27 | } 28 | -------------------------------------------------------------------------------- /test-client/src/load-testing/load-test.ts: -------------------------------------------------------------------------------- 1 | import { Worker } from 'worker_threads'; 2 | import { SyncOptions } from '../stream.js'; 3 | 4 | export type Mode = 'http' | 'websocket'; 5 | 6 | export async function stream(i: number, request: SyncOptions, print: string | undefined) { 7 | const worker = new Worker(new URL('./load-test-worker.js', import.meta.url), { 8 | workerData: { i, request, print } 9 | }); 10 | await new Promise((resolve, reject) => { 11 | worker.on('message', (event) => resolve(event)); 12 | worker.on('error', (err) => reject(err)); 13 | worker.on('exit', (__code) => { 14 | resolve(null); 15 | }); 16 | }); 17 | worker.terminate(); 18 | } 19 | 20 | export async function concurrentConnections(options: SyncOptions, numClients: number, print: string | undefined) { 21 | for (let i = 0; i < numClients; i++) { 22 | stream(i, { ...options, clientId: options.clientId ?? `load-test-${i}` }, print); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libs/lib-services/src/signals/probes/memory-probes.ts: -------------------------------------------------------------------------------- 1 | import { ProbeModule, ProbeState } from './probes.js'; 2 | 3 | export type ProbeParams = { 4 | poll_timeout_ms: number; 5 | }; 6 | 7 | export const createInMemoryProbe = (params?: ProbeParams): ProbeModule => { 8 | const state: ProbeState = { 9 | ready: false, 10 | started: false, 11 | touched_at: new Date() 12 | }; 13 | 14 | return { 15 | poll_timeout: params?.poll_timeout_ms ?? 10000, 16 | 17 | state: () => { 18 | return { 19 | ready: state.ready, 20 | started: state.started, 21 | touched_at: state.touched_at 22 | }; 23 | }, 24 | ready: async () => { 25 | state.ready = true; 26 | state.started = true; 27 | state.touched_at = new Date(); 28 | }, 29 | unready: async () => { 30 | state.ready = false; 31 | }, 32 | touch: async () => { 33 | state.touched_at = new Date(); 34 | } 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /libs/lib-services/src/migrations/migration-definitions.ts: -------------------------------------------------------------------------------- 1 | export type MigrationFunction = (context: Context) => Promise; 2 | 3 | export type Migration = { 4 | name: string; 5 | up: MigrationFunction; 6 | down: MigrationFunction; 7 | }; 8 | 9 | export enum Direction { 10 | Up = 'up', 11 | Down = 'down' 12 | } 13 | 14 | export type ExecutedMigration = { 15 | name: string; 16 | direction: Direction; 17 | timestamp: Date; 18 | }; 19 | 20 | export type MigrationState = { 21 | last_run: string; 22 | log: ExecutedMigration[]; 23 | }; 24 | 25 | export type MigrationStore = { 26 | init?: () => Promise; 27 | load: () => Promise; 28 | save: (state: MigrationState) => Promise; 29 | /** 30 | * Resets the migration store state. Mostly used for tests. 31 | */ 32 | clear: () => Promise; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/service-core/src/entry/commands/start-action.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | 3 | import * as utils from '../../util/util-index.js'; 4 | import { extractRunnerOptions, wrapConfigCommand } from './config-command.js'; 5 | 6 | const COMMAND_NAME = 'start'; 7 | 8 | export function registerStartAction(program: Command, handlers: Record) { 9 | const startCommand = program.command(COMMAND_NAME); 10 | 11 | wrapConfigCommand(startCommand); 12 | 13 | return startCommand 14 | .description('Starts a PowerSync service runner.') 15 | .option( 16 | `-r, --runner-type [${Object.values(utils.ServiceRunner).join('|')}]`, 17 | 'Type of runner to start. Defaults to unified runner.', 18 | utils.env.PS_RUNNER_TYPE 19 | ) 20 | .action(async (options) => { 21 | const runner = handlers[options.runnerType as utils.ServiceRunner]; 22 | await runner(extractRunnerOptions(options)); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/migrations/migration-utils.ts: -------------------------------------------------------------------------------- 1 | import * as lib_postgres from '@powersync/lib-service-postgres'; 2 | import { configFile } from '@powersync/service-types'; 3 | import { isPostgresStorageConfig, normalizePostgresStorageConfig, PostgresStorageConfig } from '../types/types.js'; 4 | import { STORAGE_SCHEMA_NAME } from '../utils/db.js'; 5 | import { ServiceAssertionError } from '@powersync/lib-services-framework'; 6 | import { getStorageApplicationName } from '../utils/application-name.js'; 7 | 8 | export const openMigrationDB = (config: configFile.BaseStorageConfig) => { 9 | if (!isPostgresStorageConfig(config)) { 10 | throw new ServiceAssertionError(`Input storage configuration is not for Postgres`); 11 | } 12 | return new lib_postgres.DatabaseClient({ 13 | config: normalizePostgresStorageConfig(PostgresStorageConfig.decode(config)), 14 | schema: STORAGE_SCHEMA_NAME, 15 | applicationName: getStorageApplicationName() 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /test-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-client", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "private": true, 5 | "version": "0.1.62", 6 | "main": "dist/index.js", 7 | "bin": "dist/bin.js", 8 | "license": "Apache-2.0", 9 | "type": "module", 10 | "scripts": { 11 | "fetch-operations": "tsc -b && node dist/bin.js fetch-operations", 12 | "generate-token": "tsc -b && node dist/bin.js generate-token", 13 | "concurrent-connections": "tsc -b && node dist/bin.js concurrent-connections", 14 | "build": "tsc -b", 15 | "clean": "rm -rf ./dist && tsc -b --clean" 16 | }, 17 | "dependencies": { 18 | "@powersync/service-core": "workspace:*", 19 | "commander": "^12.0.0", 20 | "jose": "^4.15.1", 21 | "undici": "^7.15.0", 22 | "ws": "^8.18.0", 23 | "yaml": "^2.5.0" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^22.16.2", 27 | "@types/ws": "~8.2.0", 28 | "typescript": "^5.7.3" 29 | } 30 | } -------------------------------------------------------------------------------- /libs/lib-services/src/utils/BaseObserver.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuid } from 'uuid'; 2 | 3 | export interface ObserverClient { 4 | registerListener(listener: Partial): () => void; 5 | } 6 | 7 | export class BaseObserver implements ObserverClient { 8 | protected listeners: { [id: string]: Partial }; 9 | 10 | constructor() { 11 | this.listeners = {}; 12 | } 13 | 14 | registerListener(listener: Partial): () => void { 15 | const id = uuid(); 16 | this.listeners[id] = listener; 17 | return () => { 18 | delete this.listeners[id]; 19 | }; 20 | } 21 | 22 | iterateListeners(cb: (listener: Partial) => any) { 23 | for (let i in this.listeners) { 24 | cb(this.listeners[i]); 25 | } 26 | } 27 | 28 | async iterateAsyncListeners(cb: (listener: Partial) => Promise) { 29 | for (let i in this.listeners) { 30 | await cb(this.listeners[i]); 31 | } 32 | } 33 | 34 | clearListeners() { 35 | this.listeners = {}; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/service-core/src/entry/commands/teardown-action.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | 3 | import { teardown } from '../../runner/teardown.js'; 4 | import { extractRunnerOptions, wrapConfigCommand } from './config-command.js'; 5 | import { ErrorCode, ServiceError } from '@powersync/lib-services-framework'; 6 | 7 | const COMMAND_NAME = 'teardown'; 8 | 9 | export function registerTearDownAction(program: Command) { 10 | const teardownCommand = program.command(COMMAND_NAME); 11 | 12 | wrapConfigCommand(teardownCommand); 13 | 14 | return teardownCommand 15 | .argument('[ack]', 'Type `TEARDOWN` to confirm teardown should occur') 16 | .description('Terminate all replicating sync rules, clear remote configuration and remove all data') 17 | .action(async (ack, options) => { 18 | if (ack !== 'TEARDOWN') { 19 | throw new ServiceError(ErrorCode.PSYNC_S0102, 'TEARDOWN was not acknowledged.'); 20 | } 21 | 22 | await teardown(extractRunnerOptions(options)); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/sync-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-sync-rules", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "version": "0.29.9", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "license": "FSL-1.1-ALv2", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "files": [ 12 | "dist/**/*", 13 | "schema/*" 14 | ], 15 | "type": "module", 16 | "scripts": { 17 | "clean": "rm -r ./dist && tsc -b --clean", 18 | "build:tsc": "tsc -b", 19 | "build": "pnpm build:tsc && node scripts/compile-schema.js", 20 | "build:tests": "tsc -b test/tsconfig.json", 21 | "test": "vitest" 22 | }, 23 | "dependencies": { 24 | "@powersync/service-jsonbig": "workspace:^", 25 | "@syncpoint/wkx": "^0.5.0", 26 | "ajv": "^8.12.0", 27 | "pgsql-ast-parser": "^11.1.0", 28 | "uuid": "^11.1.0", 29 | "yaml": "^2.3.1" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^22.16.2", 33 | "vitest": "^3.2.4" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /service/src/runners/server.ts: -------------------------------------------------------------------------------- 1 | import { container, logger } from '@powersync/lib-services-framework'; 2 | import * as core from '@powersync/service-core'; 3 | 4 | import { logBooting } from '../util/version.js'; 5 | 6 | /** 7 | * Starts an API server 8 | */ 9 | export async function startServer(runnerConfig: core.utils.RunnerConfig) { 10 | logBooting('API Container'); 11 | 12 | const config = await core.utils.loadConfig(runnerConfig); 13 | 14 | const moduleManager = container.getImplementation(core.modules.ModuleManager); 15 | 16 | const serviceContext = new core.system.ServiceContextContainer({ 17 | serviceMode: core.system.ServiceContextMode.API, 18 | configuration: config 19 | }); 20 | 21 | await moduleManager.initialize(serviceContext); 22 | 23 | logger.info('Starting service...'); 24 | await serviceContext.lifeCycleEngine.start(); 25 | logger.info('Service started.'); 26 | 27 | await container.probes.ready(); 28 | 29 | // Enable in development to track memory usage: 30 | // trackMemoryUsage(); 31 | } 32 | -------------------------------------------------------------------------------- /libs/lib-services/test/src/fixtures/schema.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | definitions: { 3 | c: { 4 | type: 'object', 5 | properties: { 6 | prop: { 7 | type: 'string' 8 | } 9 | }, 10 | required: ['prop'] 11 | } 12 | }, 13 | 14 | type: 'object', 15 | properties: { 16 | name: { 17 | type: 'object', 18 | properties: { 19 | a: { 20 | type: 'string' 21 | }, 22 | b: { 23 | enum: ['A'] 24 | } 25 | }, 26 | required: ['a'], 27 | additionalProperties: false 28 | }, 29 | b: { 30 | oneOf: [ 31 | { 32 | type: 'object', 33 | properties: { 34 | a: { 35 | type: 'number' 36 | } 37 | }, 38 | required: ['a'], 39 | additionalProperties: false 40 | } 41 | ] 42 | }, 43 | d: { 44 | $ref: '#/definitions/c' 45 | } 46 | }, 47 | required: ['name', 'b'], 48 | additionalProperties: false 49 | }; 50 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | See our [Community Code of Conduct](https://www.powersync.com/legal/community-code-of-conduct). 4 | 5 | ## I Have a Question 6 | 7 | Before asking a question, please make sure to: 8 | 9 | - Search the available [Documentation](https://docs.powersync.com/). 10 | - Review existing threads in our [Discord](https://discord.gg/powersync). 11 | - Check open [Github Issues](https://github.com/powersync-ja/powersync-service/issues). 12 | 13 | If you still need help or clarification, you can: 14 | 15 | - Ask in our Discord channels: [casual-help](https://discord.com/channels/1138230179878154300/1194710422960472175) or [help-and-questions](https://discord.com/channels/1138230179878154300/1138251135929548850). 16 | - Open a new [issue](https://github.com/powersync-ja/powersync-service/issues/new) on GitHub. 17 | - Email [support](mailto:support@powersync.com). 18 | 19 | Please provide as much context as possible, including relevant project and platform versions (Node.js, NPM, etc.). 20 | 21 | We will then get back to you as soon as possible. 22 | -------------------------------------------------------------------------------- /packages/service-core/src/storage/PersistedSyncRulesContent.ts: -------------------------------------------------------------------------------- 1 | import { SqlSyncRules } from '@powersync/service-sync-rules'; 2 | import { ReplicationLock } from './ReplicationLock.js'; 3 | 4 | export interface ParseSyncRulesOptions { 5 | defaultSchema: string; 6 | } 7 | 8 | export interface PersistedSyncRulesContent { 9 | readonly id: number; 10 | readonly sync_rules_content: string; 11 | readonly slot_name: string; 12 | /** 13 | * True if this is the "active" copy of the sync rules. 14 | */ 15 | readonly active: boolean; 16 | 17 | readonly last_checkpoint_lsn: string | null; 18 | 19 | readonly last_fatal_error?: string | null; 20 | readonly last_fatal_error_ts?: Date | null; 21 | readonly last_keepalive_ts?: Date | null; 22 | readonly last_checkpoint_ts?: Date | null; 23 | 24 | parsed(options: ParseSyncRulesOptions): PersistedSyncRules; 25 | 26 | lock(): Promise; 27 | } 28 | 29 | export interface PersistedSyncRules { 30 | readonly id: number; 31 | readonly sync_rules: SqlSyncRules; 32 | readonly slot_name: string; 33 | } 34 | -------------------------------------------------------------------------------- /packages/schema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-schema", 3 | "version": "1.18.1", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "license": "FSL-1.1-ALv2", 7 | "type": "module", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "files": [ 12 | "dist/**/*", 13 | "json-schema/*" 14 | ], 15 | "repository": "https://github.com/powersync-ja/powersync-service", 16 | "scripts": { 17 | "clean": "rm -r ./dist && tsc -b --clean", 18 | "build:ts": "tsc -b", 19 | "build": "pnpm build:ts && node ./dist/scripts/compile-json-schema.js" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": { 23 | "@powersync/service-module-postgres": "workspace:*", 24 | "@powersync/service-module-postgres-storage": "workspace:*", 25 | "@powersync/service-module-mongodb": "workspace:*", 26 | "@powersync/service-module-mongodb-storage": "workspace:*", 27 | "@powersync/service-module-mysql": "workspace:*", 28 | "@powersync/service-types": "workspace:*", 29 | "ts-codec": "^1.3.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/jpgwire/ca/README.md: -------------------------------------------------------------------------------- 1 | ## AWS RDS 2 | 3 | https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions 4 | 5 | https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem. 6 | 7 | ## Azure 8 | 9 | https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/how-to-connect-tls-ssl 10 | https://learn.microsoft.com/en-us/azure/postgresql/single-server/concepts-certificate-rotation 11 | 12 | Includes: 13 | 14 | - BaltimoreCyberTrustRoot 15 | - DigiCertGlobalRootG2 Root CA 16 | - Microsoft RSA Root Certificate Authority 2017 17 | - Microsoft ECC Root Certificate Authority 2017 18 | - DigiCert Global Root G3 19 | - DigiCert Global Root CA 20 | 21 | ## Supabase 22 | 23 | Downloaded from Supabase web portal, expires 2031. 24 | 25 | ## GCP Cloud SQL for PostgreSQL 26 | 27 | Does not appear to have a global CA. 28 | 29 | ## ISRG Root X1 30 | 31 | This one is for Let's Encrypt, used by some providers such as nhost. 32 | 33 | Using the cross-signed root does not work for some reason, but the self-signed one works. 34 | -------------------------------------------------------------------------------- /libs/lib-services/src/locks/LockManager.ts: -------------------------------------------------------------------------------- 1 | export class LockActiveError extends Error { 2 | constructor() { 3 | super('Lock is already active'); 4 | this.name = this.constructor.name; 5 | } 6 | } 7 | 8 | export type LockAcquireOptions = { 9 | /** 10 | * Optionally retry and wait for the lock to be acquired 11 | */ 12 | max_wait_ms?: number; 13 | }; 14 | 15 | export type LockHandle = { 16 | refresh(): Promise; 17 | release(): Promise; 18 | }; 19 | 20 | export type LockCallback = (refresh: () => Promise) => Promise; 21 | 22 | export type LockManager = { 23 | init?(): Promise; 24 | /** 25 | * Attempts to acquire a lock handle. 26 | * @returns null if the lock is in use and either no `max_wait_ms` was provided or the timeout has elapsed. 27 | */ 28 | acquire(options?: LockAcquireOptions): Promise; 29 | /** 30 | * Acquires a lock, executes the given handler callback then automatically releases the lock. 31 | */ 32 | lock(handler: LockCallback, options?: LockAcquireOptions): Promise; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/sync-rules/test/matchers.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect } from 'vitest'; 2 | import { SqlRuleError } from '../src/index.js'; 3 | 4 | beforeAll(() => { 5 | expect.extend({ 6 | toBeSqlRuleError(received, expectedMessage, expectedLocation) { 7 | const { isNot } = this; 8 | 9 | const message = () => { 10 | return `expected ${received} ${isNot ? ' not' : ''} to be SQL error with ${expectedMessage} at ${expectedLocation}`; 11 | }; 12 | 13 | if (received instanceof SqlRuleError) { 14 | const actualLocation = 15 | received.location && received.sql.substring(received.location.start, received.location.end); 16 | 17 | return { 18 | pass: received.message == expectedMessage && actualLocation == expectedLocation, 19 | actual: { 20 | message: received.message, 21 | location: actualLocation 22 | }, 23 | message 24 | }; 25 | } else { 26 | return { 27 | pass: false, 28 | message 29 | }; 30 | } 31 | } 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /service/local-dev/powersync-template.yaml: -------------------------------------------------------------------------------- 1 | # powersync.yaml 2 | # Very similar to the config on Collide, but a little more low-level 3 | # See https://github.com/powersync-ja/powersync-service/blob/c6140e883a4a2ac9c8c2e46b7c31ad38e1c6d28a/packages/types/src/config/PowerSyncConfig.ts#L95 4 | 5 | migrations: 6 | disable_auto_migration: false 7 | 8 | replication: 9 | connections: 10 | - type: postgresql 11 | uri: postgres://postgres:mypassword@localhost:5432/postgres 12 | sslmode: disable # okay for local/private network, not for public network 13 | 14 | storage: 15 | type: mongodb 16 | uri: mongodb://localhost:27017/powersync_demo 17 | 18 | port: 8080 19 | 20 | sync_rules: 21 | # This path is relative to the `powersync.yaml` file 22 | path: sync-rules.yaml 23 | 24 | # Client (application end user) authentication settings 25 | client_auth: 26 | # Enable this if using Supabase Auth 27 | # supabase: true 28 | 29 | # JWKS URIs can be specified here 30 | jwks_uri: http://localhost:6060/api/auth/keys 31 | 32 | # JWKS audience 33 | audience: ['powersync-dev', 'powersync'] 34 | -------------------------------------------------------------------------------- /packages/service-core/src/util/config/sync-rules/impl/filesystem-sync-rules-collector.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { RunnerConfig, SyncRulesConfig } from '../../types.js'; 3 | import { SyncRulesCollector } from '../sync-collector.js'; 4 | import { configFile } from '@powersync/service-types'; 5 | 6 | export class FileSystemSyncRulesCollector extends SyncRulesCollector { 7 | get name(): string { 8 | return 'FileSystem'; 9 | } 10 | 11 | async collect(baseConfig: configFile.PowerSyncConfig, runnerConfig: RunnerConfig): Promise { 12 | const sync_path = baseConfig.sync_rules?.path; 13 | if (!sync_path) { 14 | return null; 15 | } 16 | 17 | const { config_path } = runnerConfig; 18 | 19 | // Depending on the container, the sync rules may not actually be present. 20 | // Only persist the path here, and load on demand using `loadSyncRules()`. 21 | return { 22 | present: true, 23 | exit_on_error: baseConfig.sync_rules?.exit_on_error ?? true, 24 | path: config_path ? path.resolve(path.dirname(config_path), sync_path) : sync_path 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /libs/lib-mongodb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/lib-service-mongodb", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "types": "dist/index.d.ts", 5 | "version": "0.6.15", 6 | "main": "dist/index.js", 7 | "license": "FSL-1.1-ALv2", 8 | "type": "module", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "build": "tsc -b", 14 | "build:tests": "tsc -b test/tsconfig.json", 15 | "clean": "rm -rf ./dist && tsc -b --clean", 16 | "test": "vitest" 17 | }, 18 | "exports": { 19 | ".": { 20 | "import": "./dist/index.js", 21 | "require": "./dist/index.js", 22 | "default": "./dist/index.js" 23 | }, 24 | "./types": { 25 | "import": "./dist/types/types.js", 26 | "require": "./dist/types/types.js", 27 | "default": "./dist/types/types.js" 28 | } 29 | }, 30 | "dependencies": { 31 | "@powersync/lib-services-framework": "workspace:*", 32 | "bson": "^6.10.4", 33 | "mongodb": "^6.20.0", 34 | "mongodb-connection-string-url": "^3.0.2", 35 | "ts-codec": "^1.3.0" 36 | }, 37 | "devDependencies": {} 38 | } -------------------------------------------------------------------------------- /libs/lib-services/src/schema/validators/ts-codec-validator.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | 3 | import * as schema_validator from './schema-validator.js'; 4 | import * as defs from '../definitions.js'; 5 | import * as codecs from '../../codec/codec-index.js'; 6 | 7 | export type TsCodecValidator< 8 | C extends t.AnyCodec, 9 | T extends t.TransformTarget = t.TransformTarget.Encoded 10 | > = T extends t.TransformTarget.Encoded ? defs.MicroValidator> : defs.MicroValidator>; 11 | 12 | type ValidatorOptions = Partial> & { 13 | target?: T; 14 | }; 15 | 16 | /** 17 | * Create a validator from a given ts-codec codec 18 | */ 19 | export const createTsCodecValidator = ( 20 | codec: C, 21 | options?: ValidatorOptions 22 | ): TsCodecValidator => { 23 | const schema = t.generateJSONSchema(codec, { 24 | ...(options || {}), 25 | parsers: [...(options?.parsers ?? []), ...codecs.parsers] 26 | }); 27 | return schema_validator.createSchemaValidator(schema); 28 | }; 29 | -------------------------------------------------------------------------------- /libs/lib-services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/lib-services-framework", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "version": "0.7.13", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "types": "dist/index.d.ts", 8 | "license": "FSL-1.1-ALv2", 9 | "files": [ 10 | "dist/**/*" 11 | ], 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "scripts": { 16 | "clean": "rm -r ./dist && tsc -b --clean", 17 | "build": "tsc -b", 18 | "build:tests": "tsc -b test/tsconfig.json", 19 | "test": "vitest" 20 | }, 21 | "keywords": [], 22 | "dependencies": { 23 | "@powersync/service-errors": "workspace:*", 24 | "@powersync/service-sync-rules": "workspace:*", 25 | "ajv": "^8.12.0", 26 | "better-ajv-errors": "^1.2.0", 27 | "bson": "^6.10.4", 28 | "dotenv": "^16.4.5", 29 | "ipaddr.js": "^2.1.0", 30 | "lodash": "^4.17.21", 31 | "ts-codec": "^1.3.0", 32 | "uuid": "^11.1.0", 33 | "winston": "^3.13.0", 34 | "zod": "^3.23.8" 35 | }, 36 | "devDependencies": { 37 | "@types/lodash": "^4.17.5", 38 | "vitest": "^3.2.4" 39 | } 40 | } -------------------------------------------------------------------------------- /packages/service-core/src/util/util-index.ts: -------------------------------------------------------------------------------- 1 | export * from './alerting.js'; 2 | export * from './env.js'; 3 | export * from './lsn.js'; 4 | export * from './memory-tracking.js'; 5 | export * from './Mutex.js'; 6 | export * from './protocol-types.js'; 7 | export * from './secs.js'; 8 | export * from './utils.js'; 9 | export * from './checkpointing.js'; 10 | export * from './version.js'; 11 | 12 | export * from './config.js'; 13 | export * from './config/compound-config-collector.js'; 14 | export * from './config/types.js'; 15 | 16 | export * from './config/collectors/config-collector.js'; 17 | export * from './config/collectors/impl/base64-config-collector.js'; 18 | export * from './config/collectors/impl/fallback-config-collector.js'; 19 | export * from './config/collectors/impl/filesystem-config-collector.js'; 20 | 21 | export * from './config/sync-rules/impl/base64-sync-rules-collector.js'; 22 | export * from './config/sync-rules/impl/filesystem-sync-rules-collector.js'; 23 | export * from './config/sync-rules/impl/inline-sync-rules-collector.js'; 24 | export * from './config/sync-rules/sync-collector.js'; 25 | export * from './config/sync-rules/sync-rules-provider.js'; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./packages/jpgwire" 6 | }, 7 | { 8 | "path": "./packages/jsonbig" 9 | }, 10 | { 11 | "path": "./packages/rsocket-router" 12 | }, 13 | { 14 | "path": "./packages/service-core" 15 | }, 16 | { 17 | "path": "./packages/service-errors" 18 | }, 19 | { 20 | "path": "./packages/sync-rules" 21 | }, 22 | { 23 | "path": "./modules/module-postgres" 24 | }, 25 | { 26 | "path": "./modules/module-postgres-storage" 27 | }, 28 | { 29 | "path": "./modules/module-mssql" 30 | }, 31 | { 32 | "path": "./modules/module-mysql" 33 | }, 34 | { 35 | "path": "./modules/module-mongodb" 36 | }, 37 | { 38 | "path": "./modules/module-mongodb-storage" 39 | }, 40 | { 41 | "path": "./libs/lib-services" 42 | }, 43 | { 44 | "path": "./libs/lib-mongodb" 45 | }, 46 | { 47 | "path": "./libs/lib-postgres" 48 | }, 49 | { 50 | "path": "./packages/types" 51 | }, 52 | { 53 | "path": "./service" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/migrations/db/migrations/1741697235857-bucket-state-index.ts: -------------------------------------------------------------------------------- 1 | import { migrations } from '@powersync/service-core'; 2 | import * as storage from '../../../storage/storage-index.js'; 3 | import { MongoStorageConfig } from '../../../types/types.js'; 4 | 5 | const INDEX_NAME = 'bucket_updates'; 6 | 7 | export const up: migrations.PowerSyncMigrationFunction = async (context) => { 8 | const { 9 | service_context: { configuration } 10 | } = context; 11 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 12 | 13 | try { 14 | await db.createBucketStateIndex(); 15 | } finally { 16 | await db.client.close(); 17 | } 18 | }; 19 | 20 | export const down: migrations.PowerSyncMigrationFunction = async (context) => { 21 | const { 22 | service_context: { configuration } 23 | } = context; 24 | 25 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 26 | 27 | try { 28 | if (await db.bucket_state.indexExists(INDEX_NAME)) { 29 | await db.bucket_state.dropIndex(INDEX_NAME); 30 | } 31 | } finally { 32 | await db.client.close(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/migrations/db/migrations/1760433882550-bucket-state-index2.ts: -------------------------------------------------------------------------------- 1 | import { migrations } from '@powersync/service-core'; 2 | import * as storage from '../../../storage/storage-index.js'; 3 | import { MongoStorageConfig } from '../../../types/types.js'; 4 | 5 | const INDEX_NAME = 'dirty_buckets'; 6 | 7 | export const up: migrations.PowerSyncMigrationFunction = async (context) => { 8 | const { 9 | service_context: { configuration } 10 | } = context; 11 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 12 | 13 | try { 14 | await db.createBucketStateIndex2(); 15 | } finally { 16 | await db.client.close(); 17 | } 18 | }; 19 | 20 | export const down: migrations.PowerSyncMigrationFunction = async (context) => { 21 | const { 22 | service_context: { configuration } 23 | } = context; 24 | 25 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 26 | 27 | try { 28 | if (await db.bucket_state.indexExists(INDEX_NAME)) { 29 | await db.bucket_state.dropIndex(INDEX_NAME); 30 | } 31 | } finally { 32 | await db.client.close(); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /modules/module-mongodb/src/replication/ConnectionManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@powersync/lib-services-framework'; 2 | import { NormalizedMongoConnectionConfig } from '../types/types.js'; 3 | import { MongoManager } from './MongoManager.js'; 4 | 5 | export class ConnectionManagerFactory { 6 | private readonly connectionManagers = new Set(); 7 | public readonly dbConnectionConfig: NormalizedMongoConnectionConfig; 8 | 9 | constructor(dbConnectionConfig: NormalizedMongoConnectionConfig) { 10 | this.dbConnectionConfig = dbConnectionConfig; 11 | } 12 | 13 | create() { 14 | const manager = new MongoManager(this.dbConnectionConfig); 15 | this.connectionManagers.add(manager); 16 | 17 | manager.registerListener({ 18 | onEnded: () => { 19 | this.connectionManagers.delete(manager); 20 | } 21 | }); 22 | return manager; 23 | } 24 | 25 | async shutdown() { 26 | logger.info('Shutting down MongoDB connection Managers...'); 27 | for (const manager of [...this.connectionManagers]) { 28 | await manager.end(); 29 | } 30 | logger.info('MongoDB connection Managers shutdown completed.'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/service-core/src/auth/CompoundKeyCollector.ts: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose'; 2 | import { KeySpec } from './KeySpec.js'; 3 | import { KeyCollector, KeyResult } from './KeyCollector.js'; 4 | import { AuthorizationError } from '@powersync/lib-services-framework'; 5 | 6 | export class CompoundKeyCollector implements KeyCollector { 7 | private collectors: KeyCollector[]; 8 | 9 | constructor(collectors?: KeyCollector[]) { 10 | this.collectors = collectors ?? []; 11 | } 12 | 13 | add(collector: KeyCollector) { 14 | this.collectors.push(collector); 15 | } 16 | 17 | async getKeys(): Promise { 18 | let keys: KeySpec[] = []; 19 | let errors: AuthorizationError[] = []; 20 | const promises = this.collectors.map((collector) => 21 | collector.getKeys().then((result) => { 22 | keys.push(...result.keys); 23 | errors.push(...result.errors); 24 | }) 25 | ); 26 | await Promise.all(promises); 27 | return { keys, errors }; 28 | } 29 | 30 | async noKeyFound(): Promise { 31 | const promises = this.collectors.map((collector) => collector.noKeyFound?.()); 32 | await Promise.all(promises); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/service-core/src/sync/SyncContext.ts: -------------------------------------------------------------------------------- 1 | import { Semaphore, SemaphoreInterface, withTimeout } from 'async-mutex'; 2 | 3 | export interface SyncContextOptions { 4 | maxBuckets: number; 5 | maxParameterQueryResults: number; 6 | maxDataFetchConcurrency: number; 7 | } 8 | 9 | /** 10 | * Maximum duration to wait for the mutex to become available. 11 | * 12 | * This gives an explicit error if there are mutex issues, rather than just hanging. 13 | */ 14 | const MUTEX_ACQUIRE_TIMEOUT = 30_000; 15 | 16 | /** 17 | * Represents the context in which sync happens. 18 | * 19 | * This is global to all sync requests, not per request. 20 | */ 21 | export class SyncContext { 22 | readonly maxBuckets: number; 23 | readonly maxParameterQueryResults: number; 24 | 25 | readonly syncSemaphore: SemaphoreInterface; 26 | 27 | constructor(options: SyncContextOptions) { 28 | this.maxBuckets = options.maxBuckets; 29 | this.maxParameterQueryResults = options.maxParameterQueryResults; 30 | this.syncSemaphore = withTimeout( 31 | new Semaphore(options.maxDataFetchConcurrency), 32 | MUTEX_ACQUIRE_TIMEOUT, 33 | new Error(`Timeout while waiting for data`) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/service-core/src/metrics/metrics-interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface Counter { 2 | /** 3 | * Increment the counter by the given value. Only positive numbers are valid. 4 | * @param value 5 | */ 6 | add(value: number): void; 7 | } 8 | 9 | export interface UpDownCounter { 10 | /** 11 | * Increment or decrement(if negative) the counter by the given value. 12 | * @param value 13 | */ 14 | add(value: number): void; 15 | } 16 | 17 | export interface ObservableGauge { 18 | /** 19 | * Set a value provider that provides the value for the gauge at the time of observation. 20 | * @param valueProvider 21 | */ 22 | setValueProvider(valueProvider: () => Promise): void; 23 | } 24 | 25 | export enum Precision { 26 | INT = 'int', 27 | DOUBLE = 'double' 28 | } 29 | 30 | export interface MetricMetadata { 31 | name: string; 32 | description?: string; 33 | unit?: string; 34 | precision?: Precision; 35 | } 36 | 37 | export interface MetricsFactory { 38 | createCounter(metadata: MetricMetadata): Counter; 39 | createUpDownCounter(metadata: MetricMetadata): UpDownCounter; 40 | createObservableGauge(metadata: MetricMetadata): ObservableGauge; 41 | } 42 | -------------------------------------------------------------------------------- /libs/lib-services/src/router/endpoint.ts: -------------------------------------------------------------------------------- 1 | import * as errors from '../errors/errors-index.js'; 2 | import { Endpoint, EndpointHandlerPayload } from './router-definitions.js'; 3 | 4 | /** 5 | * Executes an endpoint's definition in the correct lifecycle order: 6 | * Validations are checked. A {@link ValidationError} is thrown if validations fail. 7 | * Authorization is checked. A {@link AuthorizationError} is thrown if checks fail. 8 | */ 9 | export const executeEndpoint = async >( 10 | endpoint: Endpoint, 11 | payload: P 12 | ) => { 13 | const validation_response = await endpoint.validator?.validate(payload.params); 14 | if (validation_response && !validation_response.valid) { 15 | throw new errors.ValidationError(validation_response.errors); 16 | } 17 | const authorizer_response = await endpoint.authorize?.(payload); 18 | if (authorizer_response && !authorizer_response.authorized) { 19 | if (authorizer_response.error == null) { 20 | throw new errors.AuthorizationError(errors.ErrorCode.PSYNC_S2101, 'Authorization failed'); 21 | } 22 | throw authorizer_response.error; 23 | } 24 | 25 | return endpoint.handler(payload); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/service-core/src/auth/StaticSupabaseKeyCollector.ts: -------------------------------------------------------------------------------- 1 | import * as jose from 'jose'; 2 | import { KeySpec, KeyOptions } from './KeySpec.js'; 3 | import { KeyCollector, KeyResult } from './KeyCollector.js'; 4 | 5 | export const SUPABASE_KEY_OPTIONS: KeyOptions = { 6 | requiresAudience: ['authenticated'], 7 | maxLifetimeSeconds: 86400 * 7 + 1200 // 1 week + 20 minutes margin 8 | }; 9 | 10 | /** 11 | * Set of static keys for Supabase. 12 | * 13 | * Same as StaticKeyCollector, but with some configuration tweaks for Supabase. 14 | * 15 | * Similar to SupabaseKeyCollector, but using hardcoded keys instead of fetching 16 | * from the database. 17 | * 18 | * A key can be added both with and without a kid, in case wildcard matching is desired. 19 | */ 20 | export class StaticSupabaseKeyCollector implements KeyCollector { 21 | static async importKeys(keys: jose.JWK[]) { 22 | const parsedKeys = await Promise.all(keys.map((key) => KeySpec.importKey(key, SUPABASE_KEY_OPTIONS))); 23 | return new StaticSupabaseKeyCollector(parsedKeys); 24 | } 25 | 26 | constructor(private keys: KeySpec[]) {} 27 | 28 | async getKeys(): Promise { 29 | return { keys: this.keys, errors: [] }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/module/PostgresStorageModule.ts: -------------------------------------------------------------------------------- 1 | import { modules, system } from '@powersync/service-core'; 2 | 3 | import { PostgresMigrationAgent } from '../migrations/PostgresMigrationAgent.js'; 4 | import { PostgresStorageProvider } from '../storage/PostgresStorageProvider.js'; 5 | import { isPostgresStorageConfig, PostgresStorageConfig } from '../types/types.js'; 6 | 7 | export class PostgresStorageModule extends modules.AbstractModule { 8 | constructor() { 9 | super({ 10 | name: 'Postgres Bucket Storage' 11 | }); 12 | } 13 | 14 | async initialize(context: system.ServiceContextContainer): Promise { 15 | const { storageEngine } = context; 16 | 17 | // Register the ability to use Postgres as a BucketStorage 18 | storageEngine.registerProvider(new PostgresStorageProvider()); 19 | 20 | if (isPostgresStorageConfig(context.configuration.storage)) { 21 | context.migrations.registerMigrationAgent( 22 | new PostgresMigrationAgent(PostgresStorageConfig.decode(context.configuration.storage)) 23 | ); 24 | } 25 | } 26 | 27 | async teardown(): Promise { 28 | // Teardown for this module is implemented in the storage engine 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /libs/lib-postgres/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/lib-service-postgres", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "types": "dist/index.d.ts", 5 | "version": "0.4.17", 6 | "main": "dist/index.js", 7 | "license": "FSL-1.1-ALv2", 8 | "type": "module", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "build": "tsc -b", 14 | "build:tests": "tsc -b test/tsconfig.json", 15 | "clean": "rm -rf ./dist && tsc -b --clean", 16 | "test": "vitest" 17 | }, 18 | "exports": { 19 | ".": { 20 | "import": "./dist/index.js", 21 | "require": "./dist/index.js", 22 | "default": "./dist/index.js" 23 | }, 24 | "./types": { 25 | "import": "./dist/types/types.js", 26 | "require": "./dist/types/types.js", 27 | "default": "./dist/types/types.js" 28 | } 29 | }, 30 | "dependencies": { 31 | "@powersync/lib-services-framework": "workspace:*", 32 | "@powersync/service-jpgwire": "workspace:*", 33 | "@powersync/service-types": "workspace:*", 34 | "p-defer": "^4.0.1", 35 | "ts-codec": "^1.3.0", 36 | "uri-js": "^4.4.1", 37 | "uuid": "^11.1.0" 38 | }, 39 | "devDependencies": {} 40 | } 41 | -------------------------------------------------------------------------------- /packages/service-errors/test/src/codes.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { ErrorCode } from '../../src/codes.js'; 3 | 4 | // This file never exports anything - it is only used for checking our 5 | // error code definitions. 6 | 7 | // Step 1: Check that the codes match the syntax `PSYNC_xxxxx`. 8 | 9 | type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'; 10 | type Category = 'S' | 'R'; 11 | 12 | // Note: This generates a type union of 20k possiblities, 13 | // which could potentially slow down the TypeScript compiler. 14 | // If it does, we could switch to a simpler `PSYNC_${Category}${number}` type. 15 | type ServiceErrorCode = `PSYNC_${Category}${Digit}${Digit}${Digit}${Digit}`; 16 | 17 | describe('Service Error Codes', () => { 18 | it('should match PSYNC_xxxxx', () => { 19 | // tsc checks this for us 20 | null as unknown as ErrorCode satisfies ServiceErrorCode; 21 | }); 22 | 23 | it('should have matching keys and values', () => { 24 | const codes = Object.keys(ErrorCode); 25 | expect(codes.length).toBeGreaterThan(40); 26 | for (let key of codes) { 27 | const value = (ErrorCode as any)[key]; 28 | expect(value).toEqual(key); 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "references": [ 12 | { 13 | "path": "../packages/jpgwire" 14 | }, 15 | { 16 | "path": "../packages/jsonbig" 17 | }, 18 | { 19 | "path": "../packages/rsocket-router" 20 | }, 21 | { 22 | "path": "../packages/service-core" 23 | }, 24 | { 25 | "path": "../packages/sync-rules" 26 | }, 27 | { 28 | "path": "../packages/types" 29 | }, 30 | { 31 | "path": "../libs/lib-services" 32 | }, 33 | { 34 | "path": "../modules/module-core" 35 | }, 36 | { 37 | "path": "../modules/module-postgres" 38 | }, 39 | { 40 | "path": "../modules/module-mongodb" 41 | }, 42 | { 43 | "path": "../modules/module-mongodb-storage" 44 | }, 45 | { 46 | "path": "../modules/module-postgres-storage" 47 | }, 48 | { 49 | "path": "../modules/module-mysql" 50 | }, 51 | { 52 | "path": "../modules/module-mssql" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /modules/module-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-module-core", 3 | "repository": "https://github.com/powersync-ja/powersync-service", 4 | "types": "dist/index.d.ts", 5 | "version": "0.2.15", 6 | "main": "dist/index.js", 7 | "license": "FSL-1.1-ALv2", 8 | "type": "module", 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "scripts": { 13 | "build": "tsc -b", 14 | "build:tests": "tsc -b test/tsconfig.json", 15 | "clean": "rm -rf ./dist && tsc -b --clean", 16 | "test": "vitest" 17 | }, 18 | "exports": { 19 | ".": { 20 | "import": "./dist/index.js", 21 | "require": "./dist/index.js", 22 | "default": "./dist/index.js" 23 | }, 24 | "./types": { 25 | "import": "./dist/types/types.js", 26 | "require": "./dist/types/types.js", 27 | "default": "./dist/types/types.js" 28 | } 29 | }, 30 | "dependencies": { 31 | "@powersync/lib-services-framework": "workspace:*", 32 | "@powersync/service-core": "workspace:*", 33 | "@powersync/service-rsocket-router": "workspace:*", 34 | "@powersync/service-types": "workspace:*", 35 | "fastify": "^5.4.0", 36 | "@fastify/cors": "^11.1.0" 37 | }, 38 | "devDependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/migrations/db/migrations/1711543888062-write-checkpoint-index.ts: -------------------------------------------------------------------------------- 1 | import { migrations } from '@powersync/service-core'; 2 | 3 | import * as storage from '../../../storage/storage-index.js'; 4 | import { MongoStorageConfig } from '../../../types/types.js'; 5 | 6 | export const up: migrations.PowerSyncMigrationFunction = async (context) => { 7 | const { 8 | service_context: { configuration } 9 | } = context; 10 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 11 | 12 | try { 13 | await db.write_checkpoints.createIndex( 14 | { 15 | user_id: 1 16 | }, 17 | { name: 'user_id' } 18 | ); 19 | } finally { 20 | await db.client.close(); 21 | } 22 | }; 23 | 24 | export const down: migrations.PowerSyncMigrationFunction = async (context) => { 25 | const { 26 | service_context: { configuration } 27 | } = context; 28 | 29 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 30 | 31 | try { 32 | if (await db.write_checkpoints.indexExists('user_id')) { 33 | await db.write_checkpoints.dropIndex('user_id'); 34 | } 35 | } finally { 36 | await db.client.close(); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@powersync/service-image", 3 | "version": "1.18.1", 4 | "private": true, 5 | "license": "FSL-1.1-ALv2", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc -b", 9 | "watch": "nodemon -w ../ -e ts -e js --delay 1 -x node --loader ts-node/esm src/entry.ts start", 10 | "clean": "rm -rf ./lib && tsc -b --clean" 11 | }, 12 | "dependencies": { 13 | "@powersync/service-core": "workspace:*", 14 | "@powersync/lib-services-framework": "workspace:*", 15 | "@powersync/service-module-postgres": "workspace:*", 16 | "@powersync/service-module-postgres-storage": "workspace:*", 17 | "@powersync/service-module-mongodb": "workspace:*", 18 | "@powersync/service-module-mongodb-storage": "workspace:*", 19 | "@powersync/service-module-mssql": "workspace:*", 20 | "@powersync/service-module-mysql": "workspace:*", 21 | "@powersync/service-rsocket-router": "workspace:*", 22 | "@powersync/service-module-core": "workspace:*", 23 | "@sentry/node": "^10.2.0" 24 | }, 25 | "devDependencies": { 26 | "@sentry/types": "^10.2.0", 27 | "copyfiles": "^2.4.1", 28 | "nodemon": "^3.0.1", 29 | "npm-check-updates": "^16.14.4", 30 | "ts-node": "^10.9.1" 31 | } 32 | } -------------------------------------------------------------------------------- /modules/module-mssql/src/common/MSSQLSourceTableCache.ts: -------------------------------------------------------------------------------- 1 | import { SourceTable } from '@powersync/service-core'; 2 | import { MSSQLSourceTable } from './MSSQLSourceTable.js'; 3 | import { ServiceAssertionError } from '@powersync/service-errors'; 4 | 5 | export class MSSQLSourceTableCache { 6 | private cache = new Map(); 7 | 8 | set(table: MSSQLSourceTable): void { 9 | this.cache.set(table.sourceTable.objectId!, table); 10 | } 11 | 12 | /** 13 | * Updates the underlying source table of the cached MSSQLSourceTable. 14 | * @param updatedTable 15 | */ 16 | updateSourceTable(updatedTable: SourceTable) { 17 | const existingTable = this.cache.get(updatedTable.objectId!); 18 | 19 | if (!existingTable) { 20 | throw new ServiceAssertionError('Tried to update a non-existing MSSQLSourceTable in the cache'); 21 | } 22 | existingTable.updateSourceTable(updatedTable); 23 | } 24 | 25 | get(tableId: number): MSSQLSourceTable | undefined { 26 | return this.cache.get(tableId); 27 | } 28 | 29 | getAll(): MSSQLSourceTable[] { 30 | return Array.from(this.cache.values()); 31 | } 32 | 33 | delete(tableId: number): boolean { 34 | return this.cache.delete(tableId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/types/models/SourceTable.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'ts-codec'; 2 | import { bigint, hexBuffer, jsonb, jsonb_raw, pgwire_number } from '../codecs.js'; 3 | 4 | export type StoredRelationId = { 5 | object_id: string | number | undefined; 6 | }; 7 | 8 | export const ColumnDescriptor = t.object({ 9 | name: t.string, 10 | /** 11 | * The type of the column ie VARCHAR, INT, etc 12 | */ 13 | type: t.string.optional(), 14 | /** 15 | * Some data sources have a type id that can be used to identify the type of the column 16 | */ 17 | typeId: t.number.optional() 18 | }); 19 | 20 | export const SourceTable = t.object({ 21 | id: t.string, 22 | group_id: pgwire_number, 23 | connection_id: bigint, 24 | relation_id: t.Null.or(jsonb_raw()), 25 | schema_name: t.string, 26 | table_name: t.string, 27 | replica_id_columns: t.Null.or(jsonb(t.array(ColumnDescriptor))), 28 | snapshot_done: t.boolean, 29 | snapshot_total_estimated_count: t.Null.or(bigint), 30 | snapshot_replicated_count: t.Null.or(bigint), 31 | snapshot_last_key: t.Null.or(hexBuffer) 32 | }); 33 | 34 | export type SourceTable = t.Encoded; 35 | export type SourceTableDecoded = t.Decoded; 36 | -------------------------------------------------------------------------------- /packages/service-core/src/entry/commands/config-command.ts: -------------------------------------------------------------------------------- 1 | import { Command } from 'commander'; 2 | 3 | import * as util from '../../util/util-index.js'; 4 | 5 | /** 6 | * Wraps a Command with the standard config options 7 | */ 8 | export function wrapConfigCommand(command: Command) { 9 | return command 10 | .option( 11 | `-c, --config-path [path]`, 12 | 'Path (inside container) to YAML config file. Defaults to process.env.POWERSYNC_CONFIG_PATH', 13 | util.env.POWERSYNC_CONFIG_PATH 14 | ) 15 | .option( 16 | `-c64, --config-base64 [base64]`, 17 | 'Base64 encoded YAML or JSON config file. Defaults to process.env.POWERSYNC_CONFIG_B64', 18 | util.env.POWERSYNC_CONFIG_B64 19 | ) 20 | .option( 21 | `-sync64, --sync-base64 [base64]`, 22 | 'Base64 encoded YAML Sync Rules. Defaults to process.env.POWERSYNC_SYNC_RULES_B64', 23 | util.env.POWERSYNC_SYNC_RULES_B64 24 | ); 25 | } 26 | 27 | /** 28 | * Extracts runner configuration params from Command options. 29 | */ 30 | export function extractRunnerOptions(options: any): util.RunnerConfig { 31 | return { 32 | config_path: options.configPath, 33 | config_base64: options.configBase64, 34 | sync_rules_base64: options.syncBase64 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /modules/module-mysql/src/replication/MySQLConnectionManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@powersync/lib-services-framework'; 2 | import mysql from 'mysql2/promise'; 3 | import { MySQLConnectionManager } from './MySQLConnectionManager.js'; 4 | import { ResolvedConnectionConfig } from '../types/types.js'; 5 | 6 | export class MySQLConnectionManagerFactory { 7 | private readonly connectionManagers = new Set(); 8 | public readonly connectionConfig: ResolvedConnectionConfig; 9 | 10 | constructor(connectionConfig: ResolvedConnectionConfig) { 11 | this.connectionConfig = connectionConfig; 12 | } 13 | 14 | create(poolOptions: mysql.PoolOptions) { 15 | const manager = new MySQLConnectionManager(this.connectionConfig, poolOptions); 16 | this.connectionManagers.add(manager); 17 | 18 | manager.registerListener({ 19 | onEnded: () => { 20 | this.connectionManagers.delete(manager); 21 | } 22 | }); 23 | return manager; 24 | } 25 | 26 | async shutdown() { 27 | logger.info('Shutting down MySQL connection Managers...'); 28 | for (const manager of [...this.connectionManagers]) { 29 | await manager.end(); 30 | } 31 | logger.info('MySQL connection Managers shutdown completed.'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/module-mysql/test/src/parser-utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { matchedSchemaChangeQuery } from '@module/utils/parser-utils.js'; 3 | 4 | describe('MySQL Parser Util Tests', () => { 5 | test('matchedSchemaChangeQuery function', () => { 6 | const matcher = (tableName: string) => tableName === 'users'; 7 | 8 | // DDL matches and table name matches 9 | expect(matchedSchemaChangeQuery('ALTER TABLE users ADD COLUMN name VARCHAR(255)', [matcher])).toBeTruthy(); 10 | expect(matchedSchemaChangeQuery('DROP TABLE users', [matcher])).toBeTruthy(); 11 | expect(matchedSchemaChangeQuery('TRUNCATE TABLE users', [matcher])).toBeTruthy(); 12 | expect(matchedSchemaChangeQuery('RENAME TABLE new_users TO users', [matcher])).toBeTruthy(); 13 | 14 | // Can handle backticks in table names 15 | expect( 16 | matchedSchemaChangeQuery('ALTER TABLE `clientSchema`.`users` ADD COLUMN name VARCHAR(255)', [matcher]) 17 | ).toBeTruthy(); 18 | 19 | // DDL matches, but table name does not match 20 | expect(matchedSchemaChangeQuery('DROP TABLE clientSchema.clients', [matcher])).toBeFalsy(); 21 | // No DDL match 22 | expect(matchedSchemaChangeQuery('SELECT * FROM users', [matcher])).toBeFalsy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /modules/module-postgres/src/utils/populate_test_data.ts: -------------------------------------------------------------------------------- 1 | import { Worker } from 'node:worker_threads'; 2 | 3 | import * as pgwire from '@powersync/service-jpgwire'; 4 | 5 | // This util is actually for tests only, but we need it compiled to JS for the service to work, so it's placed in the service. 6 | 7 | export interface PopulateDataOptions { 8 | connection: pgwire.NormalizedConnectionConfig; 9 | num_transactions: number; 10 | per_transaction: number; 11 | size: number; 12 | } 13 | 14 | export async function populateData(options: PopulateDataOptions) { 15 | const WORKER_TIMEOUT = 30_000; 16 | 17 | const worker = new Worker(new URL('./populate_test_data_worker.js', import.meta.url), { 18 | workerData: options 19 | }); 20 | const timeout = setTimeout(() => { 21 | // Exits with code 1 below 22 | worker.terminate(); 23 | }, WORKER_TIMEOUT); 24 | try { 25 | return await new Promise((resolve, reject) => { 26 | worker.on('message', resolve); 27 | worker.on('error', reject); 28 | worker.on('exit', (code) => { 29 | if (code !== 0) { 30 | reject(new Error(`Populating data failed with exit code ${code}`)); 31 | } 32 | }); 33 | }); 34 | } finally { 35 | clearTimeout(timeout); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/service-core/src/modules/AbstractModule.ts: -------------------------------------------------------------------------------- 1 | import { ServiceContextContainer } from '../system/ServiceContext.js'; 2 | import { logger } from '@powersync/lib-services-framework'; 3 | import winston from 'winston'; 4 | import { PersistedSyncRulesContent } from '../storage/storage-index.js'; 5 | 6 | export interface TearDownOptions { 7 | /** 8 | * If required, tear down any configuration/state for the specific sync rules 9 | */ 10 | syncRules?: PersistedSyncRulesContent[]; 11 | } 12 | 13 | export interface AbstractModuleOptions { 14 | name: string; 15 | } 16 | 17 | export abstract class AbstractModule { 18 | protected logger: winston.Logger; 19 | 20 | protected constructor(protected options: AbstractModuleOptions) { 21 | this.logger = logger.child({ name: `Module:${options.name}` }); 22 | } 23 | 24 | /** 25 | * Initialize the module using any required services from the ServiceContext 26 | */ 27 | public abstract initialize(context: ServiceContextContainer): Promise; 28 | 29 | /** 30 | * Permanently clean up and dispose of any configuration or state for this module. 31 | */ 32 | public abstract teardown(options: TearDownOptions): Promise; 33 | 34 | public get name() { 35 | return this.options.name; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/service-core/src/storage/StorageProvider.ts: -------------------------------------------------------------------------------- 1 | import { ServiceError } from '@powersync/lib-services-framework'; 2 | import * as util from '../util/util-index.js'; 3 | import { BucketStorageFactory } from './BucketStorageFactory.js'; 4 | import { ReportStorage } from './ReportStorage.js'; 5 | 6 | export interface ActiveStorage { 7 | storage: BucketStorageFactory; 8 | reportStorage: ReportStorage; 9 | shutDown(): Promise; 10 | 11 | /** 12 | * Tear down / drop the storage permanently 13 | */ 14 | tearDown(): Promise; 15 | 16 | onFatalError?(callback: (error: ServiceError) => void): void; 17 | } 18 | 19 | export interface GetStorageOptions { 20 | // TODO: This should just be the storage config. Update once the slot name prefix coupling has been removed from the storage 21 | resolvedConfig: util.ResolvedPowerSyncConfig; 22 | } 23 | 24 | /** 25 | * Represents a provider that can create a storage instance for a specific storage type from configuration. 26 | */ 27 | export interface StorageProvider { 28 | /** 29 | * The storage type that this provider provides. 30 | * The type should match the `type` field in the config. 31 | */ 32 | type: string; 33 | 34 | getStorage(options: GetStorageOptions): Promise; 35 | } 36 | -------------------------------------------------------------------------------- /service/src/runners/unified-runner.ts: -------------------------------------------------------------------------------- 1 | import { container, logger } from '@powersync/lib-services-framework'; 2 | import * as core from '@powersync/service-core'; 3 | 4 | import { logBooting } from '../util/version.js'; 5 | import { registerReplicationServices } from './stream-worker.js'; 6 | 7 | /** 8 | * Starts an API server 9 | */ 10 | export const startUnifiedRunner = async (runnerConfig: core.utils.RunnerConfig) => { 11 | logBooting('Unified Container'); 12 | 13 | const config = await core.utils.loadConfig(runnerConfig); 14 | 15 | const moduleManager = container.getImplementation(core.modules.ModuleManager); 16 | 17 | const serviceContext = new core.system.ServiceContextContainer({ 18 | serviceMode: core.system.ServiceContextMode.UNIFIED, 19 | configuration: config 20 | }); 21 | registerReplicationServices(serviceContext); 22 | 23 | await moduleManager.initialize(serviceContext); 24 | 25 | await core.migrations.ensureAutomaticMigrations({ 26 | serviceContext 27 | }); 28 | 29 | logger.info('Starting service...'); 30 | await serviceContext.lifeCycleEngine.start(); 31 | logger.info('Service started'); 32 | 33 | await container.probes.ready(); 34 | 35 | // Enable in development to track memory usage: 36 | // trackMemoryUsage(); 37 | }; 38 | -------------------------------------------------------------------------------- /libs/lib-services/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alerts/alerts-index.js'; 2 | export * as alerts from './alerts/alerts-index.js'; 3 | 4 | export * from './codec/codec-index.js'; 5 | export * as codecs from './codec/codec-index.js'; 6 | 7 | export * from './container.js'; 8 | 9 | export * from './errors/errors-index.js'; 10 | export * as errors from './errors/errors-index.js'; 11 | 12 | export * from './logger/logger-index.js'; 13 | 14 | export * from './locks/locks-index.js'; 15 | export * as locks from './locks/locks-index.js'; 16 | 17 | export * from './migrations/migrations-index.js'; 18 | export * as migrations from './migrations/migrations-index.js'; 19 | 20 | export * from './schema/schema-index.js'; 21 | export * as schema from './schema/schema-index.js'; 22 | 23 | export * from './signals/signals-index.js'; 24 | export * as signals from './signals/signals-index.js'; 25 | 26 | export * from './router/router-index.js'; 27 | export * as router from './router/router-index.js'; 28 | 29 | export * from './system/LifeCycledSystem.js'; 30 | export * as system from './system/system-index.js'; 31 | 32 | export * from './utils/utils-index.js'; 33 | export * as utils from './utils/utils-index.js'; 34 | 35 | export * from './ip/ip-index.js'; 36 | export * as ip from './ip/ip-index.js'; 37 | -------------------------------------------------------------------------------- /modules/module-postgres/src/replication/ConnectionManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@powersync/lib-services-framework'; 2 | import { PgPoolOptions } from '@powersync/service-jpgwire'; 3 | import { NormalizedPostgresConnectionConfig } from '../types/types.js'; 4 | import { PgManager } from './PgManager.js'; 5 | 6 | export class ConnectionManagerFactory { 7 | private readonly connectionManagers = new Set(); 8 | public readonly dbConnectionConfig: NormalizedPostgresConnectionConfig; 9 | 10 | constructor(dbConnectionConfig: NormalizedPostgresConnectionConfig) { 11 | this.dbConnectionConfig = dbConnectionConfig; 12 | } 13 | 14 | create(poolOptions: PgPoolOptions) { 15 | const manager = new PgManager(this.dbConnectionConfig, { ...poolOptions }); 16 | this.connectionManagers.add(manager); 17 | 18 | manager.registerListener({ 19 | onEnded: () => { 20 | this.connectionManagers.delete(manager); 21 | } 22 | }); 23 | return manager; 24 | } 25 | 26 | async shutdown() { 27 | logger.info('Shutting down Postgres connection Managers...'); 28 | for (const manager of [...this.connectionManagers]) { 29 | await manager.end(); 30 | } 31 | logger.info('Postgres connection Managers shutdown completed.'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /libs/lib-postgres/src/utils/identifier-utils.ts: -------------------------------------------------------------------------------- 1 | import * as pgwire from '@powersync/service-jpgwire'; 2 | import { retriedQuery } from './pgwire_utils.js'; 3 | 4 | export interface DecodedPostgresIdentifier { 5 | server_id: string; 6 | database_name: string; 7 | } 8 | 9 | export const decodePostgresSystemIdentifier = (identifier: string): DecodedPostgresIdentifier => { 10 | const [server_id, database_name] = identifier.split('.'); 11 | return { server_id, database_name }; 12 | }; 13 | 14 | export const encodePostgresSystemIdentifier = (decoded: DecodedPostgresIdentifier): string => { 15 | return `${decoded.server_id}.${decoded.database_name}`; 16 | }; 17 | 18 | export const queryPostgresSystemIdentifier = async ( 19 | connection: pgwire.PgClient 20 | ): Promise => { 21 | const result = pgwire.pgwireRows( 22 | await retriedQuery( 23 | connection, 24 | /* sql */ ` 25 | SELECT 26 | current_database() AS database_name, 27 | system_identifier 28 | FROM 29 | pg_control_system(); 30 | ` 31 | ) 32 | ) as Array<{ database_name: string; system_identifier: bigint }>; 33 | 34 | return { 35 | database_name: result[0].database_name, 36 | server_id: result[0].system_identifier.toString() 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/sync-rules/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BucketDescription.js'; 2 | export * from './BucketParameterQuerier.js'; 3 | export * from './BucketSource.js'; 4 | export * from './compatibility.js'; 5 | export * from './errors.js'; 6 | export * from './events/SqlEventDescriptor.js'; 7 | export * from './events/SqlEventSourceQuery.js'; 8 | export * from './ExpressionType.js'; 9 | export * from './IdSequence.js'; 10 | export * from './json_schema.js'; 11 | export * from './request_functions.js'; 12 | export * from './schema-generators/schema-generators.js'; 13 | export * from './SourceTableInterface.js'; 14 | export * from './sql_filters.js'; 15 | export * from './sql_functions.js'; 16 | export * from './sql_support.js'; 17 | export { SqlBucketDescriptor } from './SqlBucketDescriptor.js'; 18 | export * from './SqlDataQuery.js'; 19 | export * from './SqlParameterQuery.js'; 20 | export * from './SqlSyncRules.js'; 21 | export * from './StaticSchema.js'; 22 | export { SyncStream } from './streams/stream.js'; 23 | export { STREAM_FUNCTIONS } from './streams/functions.js'; 24 | export { syncStreamFromSql } from './streams/from_sql.js'; 25 | export * from './TablePattern.js'; 26 | export * from './types.js'; 27 | export * from './types/custom_sqlite_value.js'; 28 | export * from './types/time.js'; 29 | export * from './utils.js'; 30 | -------------------------------------------------------------------------------- /packages/service-core-tests/src/test-utils/stream_utils.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@powersync/service-core'; 2 | import { JSONBig } from '@powersync/service-jsonbig'; 3 | 4 | export function putOp(table: string, data: Record): Partial { 5 | return { 6 | op: 'PUT', 7 | object_type: table, 8 | object_id: data.id, 9 | data: JSONBig.stringify(data) 10 | }; 11 | } 12 | 13 | export function removeOp(table: string, id: string): Partial { 14 | return { 15 | op: 'REMOVE', 16 | object_type: table, 17 | object_id: id 18 | }; 19 | } 20 | 21 | export function compareIds(a: utils.OplogEntry, b: utils.OplogEntry) { 22 | return a.object_id!.localeCompare(b.object_id!); 23 | } 24 | 25 | export async function oneFromAsync(source: Iterable | AsyncIterable): Promise { 26 | const items: T[] = []; 27 | for await (const item of source) { 28 | items.push(item); 29 | } 30 | if (items.length != 1) { 31 | throw new Error(`One item expected, got: ${items.length}`); 32 | } 33 | return items[0]; 34 | } 35 | 36 | export async function fromAsync(source: Iterable | AsyncIterable): Promise { 37 | const items: T[] = []; 38 | for await (const item of source) { 39 | items.push(item); 40 | } 41 | return items; 42 | } 43 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/src/utils/db.ts: -------------------------------------------------------------------------------- 1 | import * as lib_postgres from '@powersync/lib-service-postgres'; 2 | 3 | export const STORAGE_SCHEMA_NAME = 'powersync'; 4 | 5 | export const NOTIFICATION_CHANNEL = 'powersynccheckpoints'; 6 | 7 | /** 8 | * Re export for prettier to detect the tag better 9 | */ 10 | export const sql = lib_postgres.sql; 11 | 12 | export const dropTables = async (client: lib_postgres.DatabaseClient) => { 13 | // Lock a connection for automatic schema search paths 14 | await client.lockConnection(async (db) => { 15 | await db.sql`DROP TABLE IF EXISTS bucket_data`.execute(); 16 | await db.sql`DROP TABLE IF EXISTS bucket_parameters`.execute(); 17 | await db.sql`DROP TABLE IF EXISTS sync_rules`.execute(); 18 | await db.sql`DROP TABLE IF EXISTS instance`.execute(); 19 | await db.sql`DROP TABLE IF EXISTS bucket_data`.execute(); 20 | await db.sql`DROP TABLE IF EXISTS current_data`.execute(); 21 | await db.sql`DROP TABLE IF EXISTS source_tables`.execute(); 22 | await db.sql`DROP TABLE IF EXISTS write_checkpoints`.execute(); 23 | await db.sql`DROP TABLE IF EXISTS custom_write_checkpoints`.execute(); 24 | await db.sql`DROP SEQUENCE IF EXISTS op_id_sequence`.execute(); 25 | await db.sql`DROP SEQUENCE IF EXISTS sync_rules_id_sequence`.execute(); 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /modules/module-mssql/src/replication/MSSQLConnectionManagerFactory.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@powersync/lib-services-framework'; 2 | import { ResolvedMSSQLConnectionConfig } from '../types/types.js'; 3 | import { MSSQLConnectionManager } from './MSSQLConnectionManager.js'; 4 | import sql from 'mssql'; 5 | 6 | export class MSSQLConnectionManagerFactory { 7 | private readonly connectionManagers: Set; 8 | public readonly connectionConfig: ResolvedMSSQLConnectionConfig; 9 | 10 | constructor(connectionConfig: ResolvedMSSQLConnectionConfig) { 11 | this.connectionConfig = connectionConfig; 12 | this.connectionManagers = new Set(); 13 | } 14 | 15 | create(poolOptions: sql.PoolOpts) { 16 | const manager = new MSSQLConnectionManager(this.connectionConfig, poolOptions); 17 | manager.registerListener({ 18 | onEnded: () => { 19 | this.connectionManagers.delete(manager); 20 | } 21 | }); 22 | this.connectionManagers.add(manager); 23 | return manager; 24 | } 25 | 26 | async shutdown() { 27 | logger.info('Shutting down MSSQL connection Managers...'); 28 | for (const manager of this.connectionManagers.values()) { 29 | await manager.end(); 30 | } 31 | logger.info('MSSQL connection Managers shutdown completed.'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/service-core/test/src/sync/util.test.ts: -------------------------------------------------------------------------------- 1 | import { acquireSemaphoreAbortable } from '@/index.js'; 2 | import { Semaphore, SemaphoreInterface } from 'async-mutex'; 3 | import { describe, expect, test, vi } from 'vitest'; 4 | 5 | describe('acquireSemaphoreAbortable', () => { 6 | test('can acquire', async () => { 7 | const semaphore = new Semaphore(1); 8 | const controller = new AbortController(); 9 | 10 | expect(await acquireSemaphoreAbortable(semaphore, controller.signal)).not.toBe('aborted'); 11 | }); 12 | 13 | test('can cancel', async () => { 14 | const semaphore = new Semaphore(1); 15 | const controller = new AbortController(); 16 | 17 | const resolve = vi.fn(); 18 | const reject = vi.fn(); 19 | 20 | // First invocation: Lock the semaphore 21 | const result = await acquireSemaphoreAbortable(semaphore, controller.signal); 22 | expect(result).not.toBe('aborted'); 23 | const [count, release] = result as [number, SemaphoreInterface.Releaser]; 24 | 25 | acquireSemaphoreAbortable(semaphore, controller.signal).then(resolve, reject); 26 | controller.abort(); 27 | await Promise.resolve(); 28 | expect(reject).not.toHaveBeenCalled(); 29 | expect(resolve).toHaveBeenCalledWith('aborted'); 30 | 31 | // Releasing the semaphore should not invoke resolve again 32 | release(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/migrations/db/migrations/1727099539247-custom-write-checkpoint-index.ts: -------------------------------------------------------------------------------- 1 | import { migrations } from '@powersync/service-core'; 2 | import * as storage from '../../../storage/storage-index.js'; 3 | import { MongoStorageConfig } from '../../../types/types.js'; 4 | 5 | const INDEX_NAME = 'user_sync_rule_unique'; 6 | 7 | export const up: migrations.PowerSyncMigrationFunction = async (context) => { 8 | const { 9 | service_context: { configuration } 10 | } = context; 11 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 12 | 13 | try { 14 | await db.custom_write_checkpoints.createIndex( 15 | { 16 | user_id: 1, 17 | sync_rules_id: 1 18 | }, 19 | { name: INDEX_NAME, unique: true } 20 | ); 21 | } finally { 22 | await db.client.close(); 23 | } 24 | }; 25 | 26 | export const down: migrations.PowerSyncMigrationFunction = async (context) => { 27 | const { 28 | service_context: { configuration } 29 | } = context; 30 | 31 | const db = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 32 | 33 | try { 34 | if (await db.custom_write_checkpoints.indexExists(INDEX_NAME)) { 35 | await db.custom_write_checkpoints.dropIndex(INDEX_NAME); 36 | } 37 | } finally { 38 | await db.client.close(); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/module/MongoStorageModule.ts: -------------------------------------------------------------------------------- 1 | import * as lib_mongo from '@powersync/lib-service-mongodb'; 2 | import * as core from '@powersync/service-core'; 3 | import { MongoMigrationAgent } from '../migrations/MongoMigrationAgent.js'; 4 | import { MongoStorageProvider } from '../storage/storage-index.js'; 5 | import * as types from '../types/types.js'; 6 | 7 | export class MongoStorageModule extends core.modules.AbstractModule { 8 | constructor() { 9 | super({ 10 | name: 'MongoDB Bucket Storage' 11 | }); 12 | } 13 | 14 | async initialize(context: core.system.ServiceContextContainer): Promise { 15 | context.storageEngine.registerProvider(new MongoStorageProvider()); 16 | 17 | if (types.isMongoStorageConfig(context.configuration.storage)) { 18 | context.migrations.registerMigrationAgent( 19 | new MongoMigrationAgent(this.resolveConfig(context.configuration.storage)) 20 | ); 21 | } 22 | } 23 | 24 | /** 25 | * Combines base config with normalized connection settings 26 | */ 27 | private resolveConfig(config: types.MongoStorageConfig) { 28 | return { 29 | ...config, 30 | ...lib_mongo.normalizeMongoConfig(config) 31 | }; 32 | } 33 | 34 | async teardown(options: core.modules.TearDownOptions): Promise { 35 | // teardown is implemented in the storage engine 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/module-mssql/src/replication/MSSQLErrorRateLimiter.ts: -------------------------------------------------------------------------------- 1 | import { ErrorRateLimiter } from '@powersync/service-core'; 2 | import { setTimeout } from 'timers/promises'; 3 | 4 | export class MSSQLErrorRateLimiter implements ErrorRateLimiter { 5 | nextAllowed: number = Date.now(); 6 | 7 | async waitUntilAllowed(options?: { signal?: AbortSignal | undefined } | undefined): Promise { 8 | const delay = Math.max(0, this.nextAllowed - Date.now()); 9 | // Minimum delay between connections, even without errors 10 | this.setDelay(500); 11 | await setTimeout(delay, undefined, { signal: options?.signal }); 12 | } 13 | 14 | mayPing(): boolean { 15 | return Date.now() >= this.nextAllowed; 16 | } 17 | 18 | reportError(e: any): void { 19 | const message = (e.message as string) ?? ''; 20 | if (message.includes('password authentication failed')) { 21 | this.setDelay(900_000); 22 | } else if (message.includes('ENOTFOUND')) { 23 | // DNS lookup issue - incorrect URI or deleted instance 24 | this.setDelay(120_000); 25 | } else if (message.includes('ECONNREFUSED')) { 26 | // Could be fail2ban or similar 27 | this.setDelay(120_000); 28 | } else { 29 | this.setDelay(30_000); 30 | } 31 | } 32 | 33 | private setDelay(delay: number) { 34 | this.nextAllowed = Math.max(this.nextAllowed, Date.now() + delay); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/sync-rules/src/TableQuerySchema.ts: -------------------------------------------------------------------------------- 1 | import { ColumnDefinition } from './ExpressionType.js'; 2 | import { AvailableTable } from './sql_filters.js'; 3 | import { QuerySchema, SourceSchemaTable } from './types.js'; 4 | 5 | /** 6 | * Exposes a list of {@link SourceSchemaTable}s as a {@link QuerySchema} by only exposing the subset of the schema 7 | * referenced in a `FROM` clause. 8 | */ 9 | export class TableQuerySchema implements QuerySchema { 10 | constructor( 11 | private tables: SourceSchemaTable[], 12 | private alias: AvailableTable 13 | ) {} 14 | 15 | getColumn(table: string, column: string): ColumnDefinition | undefined { 16 | if (table != this.alias.nameInSchema) { 17 | return undefined; 18 | } 19 | for (let table of this.tables) { 20 | const t = table.getColumn(column); 21 | if (t != null) { 22 | return t; 23 | } 24 | } 25 | return undefined; 26 | } 27 | 28 | getColumns(table: string): ColumnDefinition[] { 29 | if (table != this.alias.nameInSchema) { 30 | return []; 31 | } 32 | let columns: Record = {}; 33 | 34 | for (let table of this.tables) { 35 | for (let col of table.getColumns()) { 36 | if (!(col.name in columns)) { 37 | columns[col.name] = col; 38 | } 39 | } 40 | } 41 | return Object.values(columns); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /libs/lib-services/src/migrations/MigrationManager.ts: -------------------------------------------------------------------------------- 1 | import { AbstractMigrationAgent, MigrationAgentGenerics, MigrationParams } from './AbstractMigrationAgent.js'; 2 | import * as defs from './migration-definitions.js'; 3 | 4 | export class MigrationManager 5 | implements AsyncDisposable 6 | { 7 | private migrations: defs.Migration[]; 8 | private _agent: AbstractMigrationAgent | null; 9 | 10 | constructor() { 11 | this.migrations = []; 12 | this._agent = null; 13 | } 14 | 15 | registerMigrationAgent(agent: AbstractMigrationAgent) { 16 | if (this._agent) { 17 | throw new Error(`A migration agent has already been registered. Only a single agent is supported.`); 18 | } 19 | this._agent = agent; 20 | } 21 | 22 | registerMigrations(migrations: defs.Migration[]) { 23 | this.migrations.push(...migrations); 24 | } 25 | 26 | async migrate(params: MigrationParams) { 27 | if (!this._agent) { 28 | throw new Error(`A migration agent has not been registered yet.`); 29 | } 30 | return this._agent.run({ 31 | ...params, 32 | migrations: this.migrations 33 | }); 34 | } 35 | 36 | async [Symbol.asyncDispose]() { 37 | return this._agent?.[Symbol.asyncDispose](); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /modules/module-mongodb-storage/src/migrations/db/migrations/1684951997326-init.ts: -------------------------------------------------------------------------------- 1 | import * as lib_mongo from '@powersync/lib-service-mongodb'; 2 | import { migrations } from '@powersync/service-core'; 3 | import * as storage from '../../../storage/storage-index.js'; 4 | import { MongoStorageConfig } from '../../../types/types.js'; 5 | 6 | export const up: migrations.PowerSyncMigrationFunction = async (context) => { 7 | const { 8 | service_context: { configuration } 9 | } = context; 10 | const database = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 11 | await lib_mongo.waitForAuth(database.db); 12 | try { 13 | await database.bucket_parameters.createIndex( 14 | { 15 | 'key.g': 1, 16 | lookup: 1, 17 | _id: 1 18 | }, 19 | { name: 'lookup1' } 20 | ); 21 | } finally { 22 | await database.client.close(); 23 | } 24 | }; 25 | 26 | export const down: migrations.PowerSyncMigrationFunction = async (context) => { 27 | const { 28 | service_context: { configuration } 29 | } = context; 30 | 31 | const database = storage.createPowerSyncMongo(configuration.storage as MongoStorageConfig); 32 | try { 33 | if (await database.bucket_parameters.indexExists('lookup')) { 34 | await database.bucket_parameters.dropIndex('lookup1'); 35 | } 36 | } finally { 37 | await database.client.close(); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /modules/module-postgres-storage/test/src/util.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | import { normalizePostgresStorageConfig, PostgresMigrationAgent } from '../../src/index.js'; 4 | import { env } from './env.js'; 5 | import { postgresTestSetup } from '../../src/utils/test-utils.js'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | export const TEST_URI = env.PG_STORAGE_TEST_URL; 11 | 12 | const BASE_CONFIG = { 13 | type: 'postgresql' as const, 14 | uri: TEST_URI, 15 | sslmode: 'disable' as const 16 | }; 17 | 18 | export const TEST_CONNECTION_OPTIONS = normalizePostgresStorageConfig(BASE_CONFIG); 19 | 20 | /** 21 | * Vitest tries to load the migrations via .ts files which fails. 22 | * For tests this links to the relevant .js files correctly 23 | */ 24 | class TestPostgresMigrationAgent extends PostgresMigrationAgent { 25 | getInternalScriptsDir(): string { 26 | return path.resolve(__dirname, '../../dist/migrations/scripts'); 27 | } 28 | } 29 | 30 | export const POSTGRES_STORAGE_SETUP = postgresTestSetup({ 31 | url: env.PG_STORAGE_TEST_URL, 32 | migrationAgent: (config) => new TestPostgresMigrationAgent(config) 33 | }); 34 | 35 | export const POSTGRES_STORAGE_FACTORY = POSTGRES_STORAGE_SETUP.factory; 36 | export const POSTGRES_REPORT_STORAGE_FACTORY = POSTGRES_STORAGE_SETUP.reportFactory; 37 | -------------------------------------------------------------------------------- /packages/jsonbig/src/json.ts: -------------------------------------------------------------------------------- 1 | import * as json from 'lossless-json'; 2 | import { isInteger, NumberParser, Replacer, Reviver, JavaScriptValue } from 'lossless-json'; 3 | import { JsonContainer } from './json_container.js'; 4 | import { stringify } from './json_stringify.js'; 5 | 6 | const numberParser: NumberParser = (value) => { 7 | return isInteger(value) ? BigInt(value) : parseFloat(value); 8 | }; 9 | 10 | const numberStringifier = { 11 | test: (value: any) => typeof value == 'number', 12 | stringify: (value: unknown) => { 13 | const text = (value as number).toString(); 14 | if (isInteger(text)) { 15 | // Used to preserve the "type". 16 | return text + '.0'; 17 | } else { 18 | return text; 19 | } 20 | } 21 | }; 22 | 23 | // Avoid a parse + serialize round-trip for JSON data. 24 | const jsonContainerStringifier = { 25 | test: (value: any) => value instanceof JsonContainer, 26 | stringify: (value: unknown) => { 27 | return (value as JsonContainer).toString(); 28 | } 29 | }; 30 | 31 | const stringifiers = [numberStringifier, jsonContainerStringifier]; 32 | 33 | export const JSONBig = { 34 | parse(text: string, reviver?: Reviver): JavaScriptValue { 35 | return json.parse(text, reviver, numberParser); 36 | }, 37 | stringify(value: any, replacer?: Replacer, space?: string | number): string { 38 | return stringify(value, replacer, space, stringifiers)!; 39 | } 40 | }; 41 | --------------------------------------------------------------------------------