velocious 1.0.486 → 1.0.488

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 (48) hide show
  1. package/README.md +1 -0
  2. package/build/configuration-types.js +19 -1
  3. package/build/configuration.js +25 -2
  4. package/build/environment-handlers/base.js +9 -0
  5. package/build/environment-handlers/node.js +105 -19
  6. package/build/packages/velocious-package.js +115 -0
  7. package/build/src/configuration-types.d.ts +57 -2
  8. package/build/src/configuration-types.d.ts.map +1 -1
  9. package/build/src/configuration-types.js +18 -2
  10. package/build/src/configuration.d.ts +9 -1
  11. package/build/src/configuration.d.ts.map +1 -1
  12. package/build/src/configuration.js +21 -3
  13. package/build/src/environment-handlers/base.d.ts +6 -0
  14. package/build/src/environment-handlers/base.d.ts.map +1 -1
  15. package/build/src/environment-handlers/base.js +9 -1
  16. package/build/src/environment-handlers/node.d.ts +18 -0
  17. package/build/src/environment-handlers/node.d.ts.map +1 -1
  18. package/build/src/environment-handlers/node.js +94 -19
  19. package/build/src/packages/velocious-package.d.ts +69 -0
  20. package/build/src/packages/velocious-package.d.ts.map +1 -0
  21. package/build/src/packages/velocious-package.js +101 -0
  22. package/build/src/sync/sync-api-client-types.d.ts +182 -0
  23. package/build/src/sync/sync-api-client-types.d.ts.map +1 -0
  24. package/build/src/sync/sync-api-client-types.js +3 -0
  25. package/build/src/sync/sync-api-client.d.ts +188 -0
  26. package/build/src/sync/sync-api-client.d.ts.map +1 -0
  27. package/build/src/sync/sync-api-client.js +307 -0
  28. package/build/src/sync/sync-api-controller.d.ts +78 -0
  29. package/build/src/sync/sync-api-controller.d.ts.map +1 -0
  30. package/build/src/sync/sync-api-controller.js +142 -0
  31. package/build/src/sync/sync-model-change-feed-service.d.ts +131 -0
  32. package/build/src/sync/sync-model-change-feed-service.d.ts.map +1 -0
  33. package/build/src/sync/sync-model-change-feed-service.js +212 -0
  34. package/build/sync/sync-api-client-types.js +77 -0
  35. package/build/sync/sync-api-client.js +342 -0
  36. package/build/sync/sync-api-controller.js +161 -0
  37. package/build/sync/sync-model-change-feed-service.js +244 -0
  38. package/build/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +4 -4
  40. package/src/configuration-types.js +19 -1
  41. package/src/configuration.js +25 -2
  42. package/src/environment-handlers/base.js +9 -0
  43. package/src/environment-handlers/node.js +105 -19
  44. package/src/packages/velocious-package.js +115 -0
  45. package/src/sync/sync-api-client-types.js +77 -0
  46. package/src/sync/sync-api-client.js +342 -0
  47. package/src/sync/sync-api-controller.js +161 -0
  48. package/src/sync/sync-model-change-feed-service.js +244 -0
@@ -31,6 +31,8 @@ import * as inflection from "inflection"
31
31
  import path from "path"
32
32
  import {AsyncLocalStorage as NodeAsyncLocalStorage} from "node:async_hooks"
33
33
  import {timingSafeEqual} from "node:crypto"
34
+ import requireContext from "require-context"
35
+ import InitializerFromRequireContext from "../database/initializer-from-require-context.js"
34
36
  import toImportSpecifier from "../utils/to-import-specifier.js"
35
37
  import {validateTimeZone} from "../time-zone.js"
36
38
 
@@ -103,7 +105,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
103
105
  for (const backendProject of backendProjects) {
104
106
  if (backendProject.frontendModels) continue
105
107
 
106
- const resourcesDir = path.join(backendProject.path, "src", "resources")
108
+ const resourcesDir = backendProject.resourcesPath || path.join(backendProject.path, "src", "resources")
107
109
  let files
108
110
 
109
111
  try {
@@ -145,6 +147,43 @@ export default class VelociousEnvironmentHandlerNode extends Base{
145
147
  }
146
148
  }
147
149
 
150
+ /**
151
+ * Loads models contributed by registered packages into the model registry,
152
+ * after the app's own `initializeModels` hook. A package whose models directory
153
+ * is absent is skipped; a package model whose name collides with an
154
+ * already-registered different class throws. Node-only (uses the filesystem), so
155
+ * it lives here rather than in the browser-bundled Configuration.
156
+ * @param {import("../configuration.js").default} configuration - Configuration instance.
157
+ * @returns {Promise<void>} - Resolves when complete.
158
+ */
159
+ async initializePackageModels(configuration) {
160
+ for (const velociousPackage of configuration.getPackages()) {
161
+ const modelsPath = velociousPackage.getModelsPath()
162
+
163
+ try {
164
+ await fs.access(modelsPath)
165
+ } catch {
166
+ continue
167
+ }
168
+
169
+ const packageRequireContext = /** @type {import("../database/initializer-from-require-context.js").ModelClassRequireContextType} */ (requireContext(modelsPath, true, /^(.+)\.js$/))
170
+ const modelClasses = configuration.getModelClasses()
171
+
172
+ for (const fileName of packageRequireContext.keys()) {
173
+ const modelClass = packageRequireContext(fileName)?.default
174
+ const existing = modelClass && modelClasses[modelClass.getModelName()]
175
+
176
+ if (existing && existing !== modelClass) {
177
+ throw new Error(`Package "${velociousPackage.getName()}" model "${modelClass.getModelName()}" collides with an already-registered model.`)
178
+ }
179
+ }
180
+
181
+ await configuration.ensureConnections({name: `Initialize ${velociousPackage.getName()} package models`}, async () => {
182
+ await new InitializerFromRequireContext({requireContext: packageRequireContext}).initialize({configuration})
183
+ })
184
+ }
185
+ }
186
+
148
187
  /**
149
188
  * Runs set configuration.
150
189
  * @param {import("../configuration.js").default} newConfiguration - New configuration.
@@ -761,32 +800,79 @@ export default class VelociousEnvironmentHandlerNode extends Base{
761
800
  * @returns {Promise<Array<import("./base.js").MigrationObjectType>>} - Resolves with the migrations.
762
801
  */
763
802
  async findMigrations() {
764
- const migrationsPath = `${this.getConfiguration().getDirectory()}/src/database/migrations`
765
- const glob = await fs.glob(`${migrationsPath}/**/*.js`)
766
- let files = []
803
+ const configuration = this.getConfiguration()
804
+ const migrationDirectories = [`${configuration.getDirectory()}/src/database/migrations`]
805
+
806
+ for (const velociousPackage of configuration.getPackages()) {
807
+ migrationDirectories.push(velociousPackage.getMigrationsPath())
808
+ }
767
809
 
768
- for await (const fullPath of glob) {
769
- const file = await path.basename(fullPath)
810
+ /** @type {Array<import("./base.js").MigrationObjectType>} */
811
+ const files = []
770
812
 
771
- const match = file.match(/^(\d{14})-(.+)\.js$/)
813
+ for (const migrationsPath of migrationDirectories) {
814
+ await this._collectMigrationsFromDirectory(migrationsPath, files)
815
+ }
772
816
 
773
- if (!match) continue
817
+ this._ensureNoMigrationTimestampCollisions(files)
774
818
 
775
- const date = parseInt(match[1])
776
- const migrationName = match[2]
777
- const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
819
+ return files.sort((migration1, migration2) => migration1.date - migration2.date)
820
+ }
778
821
 
779
- files.push({
780
- file,
781
- fullPath: `${migrationsPath}/${file}`,
782
- date,
783
- migrationClassName
784
- })
822
+ /**
823
+ * Collects migration files from one directory into `files`, preserving each
824
+ * file's real absolute path (so app and package migrations keep their own
825
+ * source location). A missing directory is skipped.
826
+ * @param {string} migrationsPath - Directory to scan.
827
+ * @param {Array<import("./base.js").MigrationObjectType>} files - Accumulator to push into.
828
+ * @returns {Promise<void>} - Resolves when complete.
829
+ */
830
+ async _collectMigrationsFromDirectory(migrationsPath, files) {
831
+ const glob = await fs.glob(`${migrationsPath}/**/*.js`)
832
+
833
+ try {
834
+ for await (const fullPath of glob) {
835
+ const file = await path.basename(fullPath)
836
+ const match = file.match(/^(\d{14})-(.+)\.js$/)
837
+
838
+ if (!match) continue
839
+
840
+ const date = parseInt(match[1])
841
+ const migrationName = match[2]
842
+ const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
843
+
844
+ files.push({file, fullPath, date, migrationClassName})
845
+ }
846
+ } catch (error) {
847
+ if (/** @type {NodeJS.ErrnoException} */ (error)?.code !== "ENOENT") {
848
+ throw error
849
+ }
785
850
  }
851
+ }
786
852
 
787
- files = files.sort((migration1, migration2) => migration1.date - migration2.date)
853
+ /**
854
+ * Throws if two migrations from different files share the same 14-digit
855
+ * timestamp. The `schema_migrations` ledger keys on the timestamp, so a silent
856
+ * collision (e.g. between the app and a package, or two packages) would leave
857
+ * the second migration un-run — a data bug. Fail loudly instead.
858
+ * @param {Array<import("./base.js").MigrationObjectType>} files - Collected migrations.
859
+ * @returns {void} - No return value.
860
+ */
861
+ _ensureNoMigrationTimestampCollisions(files) {
862
+ /** @type {Map<number, string>} */
863
+ const pathsByDate = new Map()
788
864
 
789
- return files
865
+ for (const migration of files) {
866
+ if (!migration.fullPath) continue
867
+
868
+ const existing = pathsByDate.get(migration.date)
869
+
870
+ if (existing && existing !== migration.fullPath) {
871
+ throw new Error(`Two migrations share the timestamp ${migration.date}: ${existing} and ${migration.fullPath}. Migration timestamps must be unique across the app and all packages.`)
872
+ }
873
+
874
+ pathsByDate.set(migration.date, migration.fullPath)
875
+ }
790
876
  }
791
877
 
792
878
  /**
@@ -0,0 +1,115 @@
1
+ // @ts-check
2
+
3
+ import restArgsError from "../utils/rest-args-error.js"
4
+
5
+ /**
6
+ * A Velocious package (engine): an external npm package that contributes data
7
+ * models, frontend-model resources and migrations to a consuming app. The app
8
+ * lists packages in `Configuration({packages: [...]})`; the framework then loads
9
+ * the package's `src/models`, discovers its `src/resources`, runs its
10
+ * `src/database/migrations`, and generates its frontend models into the app.
11
+ */
12
+ export default class VelociousPackage {
13
+ /**
14
+ * Wraps a plain descriptor as a VelociousPackage (or returns it unchanged when
15
+ * it already is one), so packages can be listed without importing this class.
16
+ * @param {VelociousPackage | import("../configuration-types.js").VelociousPackageDescriptor} descriptor - Package or plain descriptor.
17
+ * @returns {VelociousPackage} - The package instance.
18
+ */
19
+ static from(descriptor) {
20
+ if (descriptor instanceof VelociousPackage) {
21
+ return descriptor
22
+ }
23
+
24
+ return new VelociousPackage(descriptor)
25
+ }
26
+
27
+ /**
28
+ * Derives the containing directory of a module url without Node's `path`/`url`
29
+ * modules, so this class stays safe to import in browser/Expo bundles.
30
+ * @param {string} url - A module url (usually `import.meta.url`).
31
+ * @returns {string} - The directory path that contains the module.
32
+ */
33
+ static _directoryFromUrl(url) {
34
+ const directoryUrl = new URL(".", url)
35
+
36
+ return decodeURIComponent(directoryUrl.pathname).replace(/\/$/, "")
37
+ }
38
+
39
+ /**
40
+ * Runs constructor.
41
+ * @param {import("../configuration-types.js").VelociousPackageDescriptor} args - Package descriptor.
42
+ */
43
+ constructor({name, url, path, modelsPath, resourcesPath, migrationsPath, ...restArgs}) {
44
+ restArgsError(restArgs)
45
+
46
+ if (!name) {
47
+ throw new Error("A velocious package requires a name.")
48
+ }
49
+
50
+ if (!path && !url) {
51
+ throw new Error(`Velocious package "${name}" requires a "path" or a "url" (usually import.meta.url).`)
52
+ }
53
+
54
+ this._name = name
55
+ this._path = path || VelociousPackage._directoryFromUrl(/** @type {string} */ (url))
56
+ this._modelsPath = modelsPath
57
+ this._resourcesPath = resourcesPath
58
+ this._migrationsPath = migrationsPath
59
+ }
60
+
61
+ /**
62
+ * Runs get name.
63
+ * @returns {string} - The package name.
64
+ */
65
+ getName() {
66
+ return this._name
67
+ }
68
+
69
+ /**
70
+ * Runs get path.
71
+ * @returns {string} - The package root directory (the one that contains `src`).
72
+ */
73
+ getPath() {
74
+ return this._path
75
+ }
76
+
77
+ /**
78
+ * Runs get models path.
79
+ * @returns {string} - The package's models directory.
80
+ */
81
+ getModelsPath() {
82
+ return this._modelsPath || `${this._path}/src/models`
83
+ }
84
+
85
+ /**
86
+ * Runs get resources path.
87
+ * @returns {string} - The package's frontend-model resources directory.
88
+ */
89
+ getResourcesPath() {
90
+ return this._resourcesPath || `${this._path}/src/resources`
91
+ }
92
+
93
+ /**
94
+ * Runs get migrations path.
95
+ * @returns {string} - The package's migrations directory.
96
+ */
97
+ getMigrationsPath() {
98
+ return this._migrationsPath || `${this._path}/src/database/migrations`
99
+ }
100
+
101
+ /**
102
+ * Derives the internal backend-project entry the framework appends so the
103
+ * existing resource-discovery + frontend-model generation machinery picks up
104
+ * this package. Generated frontend models are written to the app's output.
105
+ * @param {{frontendModelsOutputPath: string | undefined}} args - The app's frontend-models output path.
106
+ * @returns {import("../configuration-types.js").BackendProjectConfiguration} - The derived backend project.
107
+ */
108
+ toBackendProjectConfiguration({frontendModelsOutputPath}) {
109
+ return {
110
+ frontendModelsOutputPath,
111
+ path: this.getPath(),
112
+ resourcesPath: this.getResourcesPath()
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,77 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @module sync-api-client-types
5
+ */
6
+
7
+ /** @typedef {{id: string | null, serverSequence: number | null, updatedAt: string} | null} SyncCursor */
8
+
9
+ /**
10
+ * @typedef {object} SyncChangeEnvelope
11
+ * @property {() => unknown} data - Sync data payload.
12
+ * @property {() => unknown} id - Sync row identifier.
13
+ * @property {() => unknown} resourceId - Resource identifier.
14
+ * @property {() => string | null} resourceType - Resource type name.
15
+ * @property {() => string} syncType - Sync operation type.
16
+ */
17
+
18
+ /**
19
+ * @typedef {object} SyncChangeApplyResult
20
+ * @property {boolean} changed - Whether the local store changed.
21
+ * @property {string | null} [resourceType] - Applied resource type override.
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} SyncResourceConfig
26
+ * @property {(args: {attributes: Record<string, unknown>, data: Record<string, unknown>, record: ?, sync: SyncChangeEnvelope}) => Promise<boolean | void> | boolean | void} [afterApply] - Optional post-save hook.
27
+ * @property {(args: {data: Record<string, unknown>, record: ?, sync: SyncChangeEnvelope}) => Promise<Record<string, unknown>> | Record<string, unknown>} attributes - Allowed attributes builder.
28
+ * @property {boolean} enabled - Whether this resource is sync-enabled.
29
+ * @property {(args: {data: Record<string, unknown>, resourceId: unknown, sync: SyncChangeEnvelope}) => Promise<?> | ?} [findRecord] - Optional upsert finder.
30
+ * @property {(args: {resourceId: unknown, sync: SyncChangeEnvelope}) => Promise<?> | ?} [findRecordForDelete] - Optional destroy finder.
31
+ * @property {?} modelClass - Velocious model class.
32
+ */
33
+
34
+ /**
35
+ * @typedef {object} SyncChangesRequest
36
+ * @property {string} authenticationToken - Auth token.
37
+ * @property {string | null} [afterId] - Last seen row id.
38
+ * @property {number} [afterServerSequence] - Last seen server sequence.
39
+ * @property {string} [afterUpdatedAt] - Last seen timestamp.
40
+ * @property {number} limit - Page size.
41
+ * @property {string | null} [upToId] - Snapshot upper-bound row id.
42
+ * @property {number} [upToServerSequence] - Snapshot upper-bound server sequence.
43
+ * @property {string} [upToUpdatedAt] - Snapshot upper-bound timestamp.
44
+ */
45
+
46
+ /**
47
+ * @typedef {object} SyncChangesResponse
48
+ * @property {string} [errorMessage] - Error message.
49
+ * @property {SyncCursor | Record<string, ?>} [nextCursor] - Next cursor.
50
+ * @property {string} [status] - Response status.
51
+ * @property {Array<unknown>} [syncs] - Sync rows.
52
+ * @property {SyncCursor | Record<string, ?>} [upToCursor] - Snapshot upper-bound cursor.
53
+ */
54
+
55
+ /**
56
+ * @typedef {object} SyncChangesResult
57
+ * @property {boolean} changed - Whether any local record changed.
58
+ * @property {number} pages - Applied page count.
59
+ * @property {Record<string, boolean>} resourceChanged - Changed flags by resource type.
60
+ * @property {Record<string, number>} resourceCounts - Applied counts by resource type.
61
+ * @property {number} syncedCount - Applied row count.
62
+ */
63
+
64
+ /**
65
+ * @typedef {object} SyncReplayItem
66
+ * @property {string | number} id - Sync id.
67
+ * @property {string} syncState - Replay state.
68
+ */
69
+
70
+ /**
71
+ * @typedef {object} SyncReplayResponse
72
+ * @property {string} [errorMessage] - Error message.
73
+ * @property {string} [status] - Response status.
74
+ * @property {Array<SyncReplayItem>} [syncs] - Replay results.
75
+ */
76
+
77
+ export {}