@powersync/service-core 0.0.0-dev-20250507132759 → 0.0.0-dev-20250507151436

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +24 -4
  2. package/dist/entry/commands/compact-action.js +4 -1
  3. package/dist/entry/commands/compact-action.js.map +1 -1
  4. package/dist/entry/commands/migrate-action.js +4 -1
  5. package/dist/entry/commands/migrate-action.js.map +1 -1
  6. package/dist/entry/commands/test-connection-action.js +4 -1
  7. package/dist/entry/commands/test-connection-action.js.map +1 -1
  8. package/dist/routes/RouterEngine.d.ts +2 -0
  9. package/dist/routes/RouterEngine.js +15 -10
  10. package/dist/routes/RouterEngine.js.map +1 -1
  11. package/dist/routes/configure-fastify.js +0 -4
  12. package/dist/routes/configure-fastify.js.map +1 -1
  13. package/dist/routes/configure-rsocket.js +0 -3
  14. package/dist/routes/configure-rsocket.js.map +1 -1
  15. package/dist/routes/endpoints/admin.js.map +1 -1
  16. package/dist/routes/endpoints/checkpointing.js.map +1 -1
  17. package/dist/routes/endpoints/route-endpoints-index.d.ts +1 -0
  18. package/dist/routes/endpoints/route-endpoints-index.js +1 -0
  19. package/dist/routes/endpoints/route-endpoints-index.js.map +1 -1
  20. package/dist/routes/endpoints/socket-route.js.map +1 -1
  21. package/dist/routes/endpoints/sync-rules.js.map +1 -1
  22. package/dist/routes/endpoints/sync-stream.js.map +1 -1
  23. package/dist/routes/router.d.ts +1 -4
  24. package/dist/routes/router.js.map +1 -1
  25. package/dist/runner/teardown.js +4 -1
  26. package/dist/runner/teardown.js.map +1 -1
  27. package/dist/system/ServiceContext.d.ts +19 -4
  28. package/dist/system/ServiceContext.js +20 -8
  29. package/dist/system/ServiceContext.js.map +1 -1
  30. package/dist/util/config/collectors/config-collector.js +7 -30
  31. package/dist/util/config/collectors/config-collector.js.map +1 -1
  32. package/dist/util/config/collectors/impl/yaml-env.d.ts +7 -0
  33. package/dist/util/config/collectors/impl/yaml-env.js +59 -0
  34. package/dist/util/config/collectors/impl/yaml-env.js.map +1 -0
  35. package/dist/util/config/compound-config-collector.js +18 -1
  36. package/dist/util/config/compound-config-collector.js.map +1 -1
  37. package/dist/util/config/types.d.ts +11 -0
  38. package/package.json +4 -4
  39. package/src/entry/commands/compact-action.ts +4 -1
  40. package/src/entry/commands/migrate-action.ts +4 -1
  41. package/src/entry/commands/test-connection-action.ts +4 -1
  42. package/src/routes/RouterEngine.ts +21 -11
  43. package/src/routes/configure-fastify.ts +2 -7
  44. package/src/routes/configure-rsocket.ts +0 -4
  45. package/src/routes/endpoints/admin.ts +5 -5
  46. package/src/routes/endpoints/checkpointing.ts +2 -2
  47. package/src/routes/endpoints/route-endpoints-index.ts +1 -0
  48. package/src/routes/endpoints/socket-route.ts +3 -3
  49. package/src/routes/endpoints/sync-rules.ts +4 -4
  50. package/src/routes/endpoints/sync-stream.ts +3 -3
  51. package/src/routes/router.ts +2 -1
  52. package/src/runner/teardown.ts +5 -1
  53. package/src/system/ServiceContext.ts +31 -12
  54. package/src/util/config/collectors/config-collector.ts +9 -36
  55. package/src/util/config/collectors/impl/yaml-env.ts +67 -0
  56. package/src/util/config/compound-config-collector.ts +22 -5
  57. package/src/util/config/types.ts +13 -0
  58. package/test/src/config.test.ts +72 -0
  59. package/tsconfig.tsbuildinfo +1 -1
@@ -1,29 +1,22 @@
1
+ import { configFile } from '@powersync/service-types';
2
+ import * as t from 'ts-codec';
1
3
  import * as yaml from 'yaml';
2
4
 
3
5
  import { schema } from '@powersync/lib-services-framework';
4
- import { configFile } from '@powersync/service-types';
5
-
6
6
  import { RunnerConfig } from '../types.js';
7
+ import { YamlEnvTag } from './impl/yaml-env.js';
7
8
 
8
9
  export enum ConfigFileFormat {
9
10
  YAML = 'yaml',
10
11
  JSON = 'json'
11
12
  }
12
13
 
13
- /**
14
- * Environment variables can be substituted into the YAML config
15
- * when parsing if the environment variable name starts with this prefix.
16
- * Attempting to substitute any other environment variable will throw an exception.
17
- *
18
- * Example of substitution:
19
- * storage:
20
- * type: mongodb
21
- * uri: !env PS_MONGO_URI
22
- */
23
- const YAML_ENV_PREFIX = 'PS_';
24
-
25
14
  // ts-codec itself doesn't give great validation errors, so we use json schema for that
26
- const configSchemaValidator = schema.parseJSONSchema(configFile.PowerSyncConfigJSONSchema).validator();
15
+ const configSchemaValidator = schema
16
+ .parseJSONSchema(
17
+ t.generateJSONSchema(configFile.powerSyncConfig, { allowAdditional: true, parsers: [configFile.portParser] })
18
+ )
19
+ .validator();
27
20
 
28
21
  export abstract class ConfigCollector {
29
22
  abstract get name(): string;
@@ -100,27 +93,7 @@ export abstract class ConfigCollector {
100
93
  schema: 'core',
101
94
  keepSourceTokens: true,
102
95
  lineCounter,
103
- customTags: [
104
- {
105
- tag: '!env',
106
- resolve(envName: string, onError: (error: string) => void) {
107
- if (!envName.startsWith(YAML_ENV_PREFIX)) {
108
- onError(
109
- `Attempting to substitute environment variable ${envName} is not allowed. Variables must start with "${YAML_ENV_PREFIX}"`
110
- );
111
- return envName;
112
- }
113
- const value = process.env[envName];
114
- if (typeof value == 'undefined') {
115
- onError(
116
- `Attempted to substitute environment variable "${envName}" which is undefined. Set this variable on the environment.`
117
- );
118
- return envName;
119
- }
120
- return value;
121
- }
122
- }
123
- ]
96
+ customTags: [YamlEnvTag]
124
97
  });
125
98
 
126
99
  if (parsed.errors.length) {
@@ -0,0 +1,67 @@
1
+ import * as yaml from 'yaml';
2
+
3
+ /**
4
+ * Environment variables can be substituted into the YAML config
5
+ * when parsing if the environment variable name starts with this prefix.
6
+ * Attempting to substitute any other environment variable will throw an exception.
7
+ *
8
+ * Example of substitution:
9
+ * storage:
10
+ * type: mongodb
11
+ * uri: !env PS_MONGO_URI
12
+ */
13
+ const YAML_ENV_PREFIX = 'PS_';
14
+
15
+ /**
16
+ * Custom YAML tag which performs string environment variable substitution
17
+ * Allows for type casting string environment variables to boolean or number
18
+ * by using the syntax !env PS_MONGO_PORT::number or !env PS_USE_SUPABASE::boolean
19
+ */
20
+ export const YamlEnvTag: yaml.ScalarTag = {
21
+ tag: '!env',
22
+ resolve(envName: string, onError: (error: string) => void) {
23
+ if (!envName.startsWith(YAML_ENV_PREFIX)) {
24
+ onError(
25
+ `Attempting to substitute environment variable ${envName} is not allowed. Variables must start with "${YAML_ENV_PREFIX}"`
26
+ );
27
+ return envName;
28
+ }
29
+
30
+ // allow type casting if the envName contains a type suffix
31
+ // e.g. PS_MONGO_PORT::number or PS_USE_SUPABASE::boolean
32
+ const [name, type = 'string'] = envName.split('::');
33
+
34
+ let value = process.env[name];
35
+
36
+ if (typeof value == 'undefined') {
37
+ onError(
38
+ `Attempted to substitute environment variable "${envName}" which is undefined. Set this variable on the environment.`
39
+ );
40
+ return envName;
41
+ }
42
+
43
+ switch (type) {
44
+ case 'string':
45
+ return value;
46
+ case 'number':
47
+ const numberValue = Number(value);
48
+ if (Number.isNaN(numberValue)) {
49
+ onError(`Environment variable "${envName}" is not a valid number. Got: "${value}".`);
50
+ return envName;
51
+ }
52
+ return numberValue;
53
+ case 'boolean':
54
+ if (value?.toLowerCase() == 'true') {
55
+ return true;
56
+ } else if (value?.toLowerCase() == 'false') {
57
+ return false;
58
+ } else {
59
+ onError(`Environment variable "${envName}" is not a boolean. Expected "true" or "false", got "${value}".`);
60
+ return envName;
61
+ }
62
+ default:
63
+ onError(`Environment variable "${envName}" has an invalid type suffix "${type}".`);
64
+ return envName;
65
+ }
66
+ }
67
+ };
@@ -5,11 +5,6 @@ import { ConfigCollector } from './collectors/config-collector.js';
5
5
  import { Base64ConfigCollector } from './collectors/impl/base64-config-collector.js';
6
6
  import { FallbackConfigCollector } from './collectors/impl/fallback-config-collector.js';
7
7
  import { FileSystemConfigCollector } from './collectors/impl/filesystem-config-collector.js';
8
- import { Base64SyncRulesCollector } from './sync-rules/impl/base64-sync-rules-collector.js';
9
- import { FileSystemSyncRulesCollector } from './sync-rules/impl/filesystem-sync-rules-collector.js';
10
- import { InlineSyncRulesCollector } from './sync-rules/impl/inline-sync-rules-collector.js';
11
- import { SyncRulesCollector } from './sync-rules/sync-collector.js';
12
- import { ResolvedPowerSyncConfig, RunnerConfig, SyncRulesConfig } from './types.js';
13
8
  import {
14
9
  DEFAULT_MAX_BUCKETS_PER_CONNECTION,
15
10
  DEFAULT_MAX_CONCURRENT_CONNECTIONS,
@@ -17,6 +12,11 @@ import {
17
12
  DEFAULT_MAX_PARAMETER_QUERY_RESULTS,
18
13
  DEFAULT_MAX_POOL_SIZE
19
14
  } from './defaults.js';
15
+ import { Base64SyncRulesCollector } from './sync-rules/impl/base64-sync-rules-collector.js';
16
+ import { FileSystemSyncRulesCollector } from './sync-rules/impl/filesystem-sync-rules-collector.js';
17
+ import { InlineSyncRulesCollector } from './sync-rules/impl/inline-sync-rules-collector.js';
18
+ import { SyncRulesCollector } from './sync-rules/sync-collector.js';
19
+ import { ResolvedPowerSyncConfig, RunnerConfig, SyncRulesConfig } from './types.js';
20
20
 
21
21
  export type CompoundConfigCollectorOptions = {
22
22
  /**
@@ -159,6 +159,23 @@ export class CompoundConfigCollector {
159
159
  internal_service_endpoint:
160
160
  baseConfig.telemetry?.internal_service_endpoint ?? 'https://pulse.journeyapps.com/v1/metrics'
161
161
  },
162
+ healthcheck: {
163
+ /**
164
+ * Default to legacy mode if no probes config is provided.
165
+ * If users provide a config, all options require explicit opt-in.
166
+ */
167
+ probes: baseConfig.healthcheck?.probes
168
+ ? {
169
+ use_filesystem: baseConfig.healthcheck.probes.use_filesystem ?? false,
170
+ use_http: baseConfig.healthcheck.probes.use_http ?? false,
171
+ use_legacy: baseConfig.healthcheck.probes.use_legacy ?? false
172
+ }
173
+ : {
174
+ use_filesystem: false,
175
+ use_http: false,
176
+ use_legacy: true
177
+ }
178
+ },
162
179
  api_parameters: {
163
180
  max_buckets_per_connection:
164
181
  baseConfig.api?.parameters?.max_buckets_per_connection ?? DEFAULT_MAX_BUCKETS_PER_CONNECTION,
@@ -69,5 +69,18 @@ export type ResolvedPowerSyncConfig = {
69
69
 
70
70
  /** Prefix for postgres replication slot names. May eventually be connection-specific. */
71
71
  slot_name_prefix: string;
72
+
73
+ healthcheck: {
74
+ probes: {
75
+ use_filesystem: boolean;
76
+ use_http: boolean;
77
+ /**
78
+ * @deprecated This maintains backwards compatibility with the legacy default.
79
+ * Explicit probe configuration should be used instead.
80
+ */
81
+ use_legacy: boolean;
82
+ };
83
+ };
84
+
72
85
  parameters: Record<string, number | string | boolean | null>;
73
86
  };
@@ -0,0 +1,72 @@
1
+ // Vitest Unit Tests
2
+ import { CompoundConfigCollector } from '@/index.js';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+
5
+ describe('Config', () => {
6
+ it('should substitute env variables in YAML config', {}, async () => {
7
+ const type = 'mongodb';
8
+ vi.stubEnv('PS_MONGO_TYPE', type);
9
+
10
+ const yamlConfig = /* yaml */ `
11
+ # PowerSync config
12
+ replication:
13
+ connections: []
14
+ storage:
15
+ type: !env PS_MONGO_TYPE
16
+ `;
17
+
18
+ const collector = new CompoundConfigCollector();
19
+
20
+ const config = await collector.collectConfig({
21
+ config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64')
22
+ });
23
+
24
+ expect(config.storage.type).toBe(type);
25
+ });
26
+
27
+ it('should substitute boolean env variables in YAML config', {}, async () => {
28
+ vi.stubEnv('PS_MONGO_HEALTHCHECK', 'true');
29
+
30
+ const yamlConfig = /* yaml */ `
31
+ # PowerSync config
32
+ replication:
33
+ connections: []
34
+ storage:
35
+ type: mongodb
36
+ healthcheck:
37
+ probes:
38
+ use_http: !env PS_MONGO_HEALTHCHECK::boolean
39
+ `;
40
+
41
+ const collector = new CompoundConfigCollector();
42
+
43
+ const config = await collector.collectConfig({
44
+ config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64')
45
+ });
46
+
47
+ expect(config.healthcheck.probes.use_http).toBe(true);
48
+ });
49
+
50
+ it('should substitute number env variables in YAML config', {}, async () => {
51
+ vi.stubEnv('PS_MAX_BUCKETS', '1');
52
+
53
+ const yamlConfig = /* yaml */ `
54
+ # PowerSync config
55
+ replication:
56
+ connections: []
57
+ storage:
58
+ type: mongodb
59
+ api:
60
+ parameters:
61
+ max_buckets_per_connection: !env PS_MAX_BUCKETS::number
62
+ `;
63
+
64
+ const collector = new CompoundConfigCollector();
65
+
66
+ const config = await collector.collectConfig({
67
+ config_base64: Buffer.from(yamlConfig, 'utf-8').toString('base64')
68
+ });
69
+
70
+ expect(config.api_parameters.max_buckets_per_connection).toBe(1);
71
+ });
72
+ });