@famgia/omnify-core 0.0.8 → 0.0.9

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.
package/dist/index.cjs CHANGED
@@ -33,6 +33,7 @@ __export(index_exports, {
33
33
  Omnify: () => Omnify,
34
34
  OmnifyError: () => OmnifyError,
35
35
  PluginManager: () => PluginManager,
36
+ VersionStore: () => VersionStore,
36
37
  atlasError: () => atlasError,
37
38
  atlasNotFoundError: () => atlasNotFoundError,
38
39
  circularReferenceError: () => circularReferenceError,
@@ -40,6 +41,7 @@ __export(index_exports, {
40
41
  configNotFoundError: () => configNotFoundError,
41
42
  createOmnify: () => createOmnify,
42
43
  createPluginManager: () => createPluginManager,
44
+ createVersionStore: () => createVersionStore,
43
45
  duplicateSchemaError: () => duplicateSchemaError,
44
46
  err: () => import_omnify_types.err,
45
47
  expandProperty: () => expandProperty,
@@ -3042,11 +3044,309 @@ var Omnify = class {
3042
3044
  function createOmnify(options) {
3043
3045
  return new Omnify(options);
3044
3046
  }
3047
+
3048
+ // src/history/version-store.ts
3049
+ var import_promises = require("fs/promises");
3050
+ var import_path = require("path");
3051
+ var import_js_yaml2 = __toESM(require("js-yaml"), 1);
3052
+ var OMNIFY_DIR = ".omnify";
3053
+ var VERSIONS_DIR = "versions";
3054
+ var CURRENT_FILE = "current.lock";
3055
+ var VERSION_FILE_PATTERN = /^(\d{4})(?:_(.+))?\.lock$/;
3056
+ function formatVersionNumber(version) {
3057
+ return version.toString().padStart(4, "0");
3058
+ }
3059
+ function parseVersionFilename(filename) {
3060
+ const match = filename.match(VERSION_FILE_PATTERN);
3061
+ if (!match || !match[1]) return null;
3062
+ const result = {
3063
+ version: parseInt(match[1], 10)
3064
+ };
3065
+ if (match[2] !== void 0) {
3066
+ result.name = match[2];
3067
+ }
3068
+ return result;
3069
+ }
3070
+ function generateVersionFilename(version, migration) {
3071
+ const versionStr = formatVersionNumber(version);
3072
+ if (migration) {
3073
+ const name = migration.replace(/^\d+_/, "").replace(/\.sql$/, "");
3074
+ return `${versionStr}_${name}.lock`;
3075
+ }
3076
+ return `${versionStr}.lock`;
3077
+ }
3078
+ async function dirExists(path2) {
3079
+ try {
3080
+ await (0, import_promises.access)(path2);
3081
+ return true;
3082
+ } catch {
3083
+ return false;
3084
+ }
3085
+ }
3086
+ var VersionStore = class {
3087
+ omnifyDir;
3088
+ versionsDir;
3089
+ maxVersions;
3090
+ constructor(config) {
3091
+ this.omnifyDir = (0, import_path.join)(config.baseDir, OMNIFY_DIR);
3092
+ this.versionsDir = (0, import_path.join)(this.omnifyDir, VERSIONS_DIR);
3093
+ this.maxVersions = config.maxVersions ?? 0;
3094
+ }
3095
+ /**
3096
+ * Initialize the version store directory structure.
3097
+ */
3098
+ async initialize() {
3099
+ await (0, import_promises.mkdir)(this.versionsDir, { recursive: true });
3100
+ }
3101
+ /**
3102
+ * Get the path to the versions directory.
3103
+ */
3104
+ getVersionsDir() {
3105
+ return this.versionsDir;
3106
+ }
3107
+ /**
3108
+ * Get the path to a specific version file.
3109
+ */
3110
+ getVersionPath(version, migration) {
3111
+ const filename = generateVersionFilename(version, migration);
3112
+ return (0, import_path.join)(this.versionsDir, filename);
3113
+ }
3114
+ /**
3115
+ * List all available versions.
3116
+ */
3117
+ async listVersions() {
3118
+ if (!await dirExists(this.versionsDir)) {
3119
+ return [];
3120
+ }
3121
+ const files = await (0, import_promises.readdir)(this.versionsDir);
3122
+ const versions = [];
3123
+ for (const file of files) {
3124
+ const parsed = parseVersionFilename(file);
3125
+ if (!parsed) continue;
3126
+ try {
3127
+ const filePath = (0, import_path.join)(this.versionsDir, file);
3128
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
3129
+ const versionFile = import_js_yaml2.default.load(content);
3130
+ const summary = {
3131
+ version: versionFile.version,
3132
+ timestamp: versionFile.timestamp,
3133
+ ...versionFile.migration !== void 0 && { migration: versionFile.migration },
3134
+ ...versionFile.description !== void 0 && { description: versionFile.description },
3135
+ schemaCount: Object.keys(versionFile.snapshot).length,
3136
+ changeCount: versionFile.changes.length
3137
+ };
3138
+ versions.push(summary);
3139
+ } catch {
3140
+ }
3141
+ }
3142
+ return versions.sort((a, b) => a.version - b.version);
3143
+ }
3144
+ /**
3145
+ * Get the latest version number.
3146
+ */
3147
+ async getLatestVersion() {
3148
+ const versions = await this.listVersions();
3149
+ if (versions.length === 0) return 0;
3150
+ const lastVersion = versions[versions.length - 1];
3151
+ return lastVersion?.version ?? 0;
3152
+ }
3153
+ /**
3154
+ * Read a specific version file.
3155
+ */
3156
+ async readVersion(version) {
3157
+ if (!await dirExists(this.versionsDir)) {
3158
+ return null;
3159
+ }
3160
+ const files = await (0, import_promises.readdir)(this.versionsDir);
3161
+ for (const file of files) {
3162
+ const parsed = parseVersionFilename(file);
3163
+ if (parsed?.version === version) {
3164
+ const filePath = (0, import_path.join)(this.versionsDir, file);
3165
+ const content = await (0, import_promises.readFile)(filePath, "utf-8");
3166
+ return import_js_yaml2.default.load(content);
3167
+ }
3168
+ }
3169
+ return null;
3170
+ }
3171
+ /**
3172
+ * Read the latest version file.
3173
+ */
3174
+ async readLatestVersion() {
3175
+ const latestVersion = await this.getLatestVersion();
3176
+ if (latestVersion === 0) return null;
3177
+ return this.readVersion(latestVersion);
3178
+ }
3179
+ /**
3180
+ * Create a new version.
3181
+ */
3182
+ async createVersion(snapshot, changes, options) {
3183
+ await this.initialize();
3184
+ const latestVersion = await this.getLatestVersion();
3185
+ const newVersion = latestVersion + 1;
3186
+ const versionFile = {
3187
+ version: newVersion,
3188
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3189
+ driver: options.driver,
3190
+ ...options.migration !== void 0 && { migration: options.migration },
3191
+ ...options.description !== void 0 && { description: options.description },
3192
+ changes,
3193
+ snapshot
3194
+ };
3195
+ const filePath = this.getVersionPath(newVersion, options.migration);
3196
+ const content = import_js_yaml2.default.dump(versionFile, {
3197
+ lineWidth: -1,
3198
+ // Disable line wrapping
3199
+ noRefs: true,
3200
+ quotingType: '"'
3201
+ });
3202
+ await (0, import_promises.writeFile)(filePath, content, "utf-8");
3203
+ await this.updateCurrentLink(versionFile);
3204
+ if (this.maxVersions > 0) {
3205
+ await this.cleanupOldVersions();
3206
+ }
3207
+ return versionFile;
3208
+ }
3209
+ /**
3210
+ * Update the current.lock file to point to latest version.
3211
+ */
3212
+ async updateCurrentLink(versionFile) {
3213
+ const currentPath = (0, import_path.join)(this.omnifyDir, CURRENT_FILE);
3214
+ const content = import_js_yaml2.default.dump(versionFile, {
3215
+ lineWidth: -1,
3216
+ noRefs: true,
3217
+ quotingType: '"'
3218
+ });
3219
+ await (0, import_promises.writeFile)(currentPath, content, "utf-8");
3220
+ }
3221
+ /**
3222
+ * Cleanup old versions beyond maxVersions limit.
3223
+ */
3224
+ async cleanupOldVersions() {
3225
+ const versions = await this.listVersions();
3226
+ if (versions.length <= this.maxVersions) return;
3227
+ const toDelete = versions.slice(0, versions.length - this.maxVersions);
3228
+ for (const v of toDelete) {
3229
+ const files = await (0, import_promises.readdir)(this.versionsDir);
3230
+ for (const file of files) {
3231
+ const parsed = parseVersionFilename(file);
3232
+ if (parsed?.version === v.version) {
3233
+ await (0, import_promises.rm)((0, import_path.join)(this.versionsDir, file));
3234
+ break;
3235
+ }
3236
+ }
3237
+ }
3238
+ }
3239
+ /**
3240
+ * Get diff between two versions.
3241
+ */
3242
+ async diffVersions(fromVersion, toVersion) {
3243
+ const fromFile = await this.readVersion(fromVersion);
3244
+ const toFile = await this.readVersion(toVersion);
3245
+ if (!fromFile || !toFile) return null;
3246
+ const allChanges = [];
3247
+ if (fromVersion < toVersion) {
3248
+ for (let v = fromVersion + 1; v <= toVersion; v++) {
3249
+ const vFile = await this.readVersion(v);
3250
+ if (vFile) {
3251
+ allChanges.push(...vFile.changes);
3252
+ }
3253
+ }
3254
+ } else {
3255
+ const changes = this.computeSnapshotDiff(fromFile.snapshot, toFile.snapshot);
3256
+ allChanges.push(...changes);
3257
+ }
3258
+ return {
3259
+ fromVersion,
3260
+ toVersion,
3261
+ changes: allChanges
3262
+ };
3263
+ }
3264
+ /**
3265
+ * Compute changes between two snapshots.
3266
+ */
3267
+ computeSnapshotDiff(from, to) {
3268
+ const changes = [];
3269
+ const fromNames = new Set(Object.keys(from));
3270
+ const toNames = new Set(Object.keys(to));
3271
+ for (const name of toNames) {
3272
+ if (!fromNames.has(name)) {
3273
+ changes.push({ action: "schema_added", schema: name });
3274
+ }
3275
+ }
3276
+ for (const name of fromNames) {
3277
+ if (!toNames.has(name)) {
3278
+ changes.push({ action: "schema_removed", schema: name });
3279
+ }
3280
+ }
3281
+ for (const name of fromNames) {
3282
+ if (!toNames.has(name)) continue;
3283
+ const fromSchema = from[name];
3284
+ const toSchema = to[name];
3285
+ if (!fromSchema || !toSchema) continue;
3286
+ const fromProps = fromSchema.properties ?? {};
3287
+ const toProps = toSchema.properties ?? {};
3288
+ const fromPropNames = new Set(Object.keys(fromProps));
3289
+ const toPropNames = new Set(Object.keys(toProps));
3290
+ for (const prop of toPropNames) {
3291
+ if (!fromPropNames.has(prop)) {
3292
+ changes.push({
3293
+ action: "property_added",
3294
+ schema: name,
3295
+ property: prop,
3296
+ to: toProps[prop]
3297
+ });
3298
+ }
3299
+ }
3300
+ for (const prop of fromPropNames) {
3301
+ if (!toPropNames.has(prop)) {
3302
+ changes.push({
3303
+ action: "property_removed",
3304
+ schema: name,
3305
+ property: prop,
3306
+ from: fromProps[prop]
3307
+ });
3308
+ }
3309
+ }
3310
+ for (const prop of fromPropNames) {
3311
+ if (!toPropNames.has(prop)) continue;
3312
+ if (JSON.stringify(fromProps[prop]) !== JSON.stringify(toProps[prop])) {
3313
+ changes.push({
3314
+ action: "property_modified",
3315
+ schema: name,
3316
+ property: prop,
3317
+ from: fromProps[prop],
3318
+ to: toProps[prop]
3319
+ });
3320
+ }
3321
+ }
3322
+ if (JSON.stringify(fromSchema.options) !== JSON.stringify(toSchema.options)) {
3323
+ changes.push({
3324
+ action: "option_changed",
3325
+ schema: name,
3326
+ from: fromSchema.options,
3327
+ to: toSchema.options
3328
+ });
3329
+ }
3330
+ }
3331
+ return changes;
3332
+ }
3333
+ /**
3334
+ * Get snapshot at a specific version (for rollback).
3335
+ */
3336
+ async getSnapshotAt(version) {
3337
+ const versionFile = await this.readVersion(version);
3338
+ return versionFile?.snapshot ?? null;
3339
+ }
3340
+ };
3341
+ function createVersionStore(config) {
3342
+ return new VersionStore(config);
3343
+ }
3045
3344
  // Annotate the CommonJS export names for ESM import in node:
3046
3345
  0 && (module.exports = {
3047
3346
  Omnify,
3048
3347
  OmnifyError,
3049
3348
  PluginManager,
3349
+ VersionStore,
3050
3350
  atlasError,
3051
3351
  atlasNotFoundError,
3052
3352
  circularReferenceError,
@@ -3054,6 +3354,7 @@ function createOmnify(options) {
3054
3354
  configNotFoundError,
3055
3355
  createOmnify,
3056
3356
  createPluginManager,
3357
+ createVersionStore,
3057
3358
  duplicateSchemaError,
3058
3359
  err,
3059
3360
  expandProperty,