@onyx.dev/onyx-database 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -42,6 +42,22 @@ npm i @onyx.dev/onyx-database
42
42
 
43
43
  The package is dual-module (ESM + CJS) and has **no runtime or peer dependencies**.
44
44
 
45
+ To use the bundled CLIs (`onyx-gen` and `onyx-schema`) globally:
46
+
47
+ ```bash
48
+ npm install -g @onyx.dev/onyx-database
49
+ ```
50
+
51
+ To install the CLI globally from this repo checkout (useful for local development and testing):
52
+
53
+ ```bash
54
+ # run from the repo root
55
+ npm install
56
+ npm run build
57
+ npm uninstall -g @onyx.dev/onyx-database # optional: clear older global versions
58
+ npm install -g . # installs the built onyx-schema and onyx-gen
59
+ ```
60
+
45
61
  ---
46
62
 
47
63
  ## Initialize the client
@@ -148,6 +164,9 @@ onyx-schema publish
148
164
  # Overwrite ./onyx.schema.json with the remote schema
149
165
  onyx-schema get
150
166
 
167
+ # Print the remote schema without writing a file
168
+ onyx-schema get --print
169
+
151
170
  # Fetch only selected tables (prints to stdout; does not overwrite files)
152
171
  onyx-schema get --tables=User,Profile
153
172
 
@@ -177,7 +196,7 @@ onyx-schema validate ./onyx.schema.json
177
196
 
178
197
  # Diff local schema vs API
179
198
  onyx-schema diff ./onyx.schema.json
180
- # Prints added/removed/changed tables and attribute differences between the API schema and your local file.
199
+ # Prints YAML with added/removed/changed tables and attribute differences between the API schema and your local file.
181
200
  ```
182
201
 
183
202
  When `--tables` is provided, the subset is printed to stdout instead of writing a
@@ -194,6 +213,16 @@ npm run schema:publish # validate then publish the local schema
194
213
  The CLI reuses the same configuration resolution as `onyx.init()` (env vars,
195
214
  project config, and home profile files).
196
215
 
216
+ Programmatic diffing is also available:
217
+
218
+ ```ts
219
+ import { onyx } from '@onyx.dev/onyx-database';
220
+
221
+ const db = onyx.init();
222
+ const diff = await db.diffSchema(localSchema); // SchemaUpsertRequest
223
+ console.log(diff.changedTables);
224
+ ```
225
+
197
226
  You can also emit to multiple paths in one run (comma-separated or by repeating `--out`):
198
227
 
199
228
  ```bash
@@ -76,7 +76,7 @@ function readEnv(targetId) {
76
76
  return res;
77
77
  }
78
78
  async function readProjectFile(databaseId) {
79
- if (!isNode) return {};
79
+ if (!isNode) return { config: {} };
80
80
  const fs = await nodeImport("node:fs/promises");
81
81
  const path = await nodeImport("node:path");
82
82
  const cwd = gProcess?.cwd?.() ?? ".";
@@ -85,7 +85,7 @@ async function readProjectFile(databaseId) {
85
85
  const sanitized = txt.replace(/[\r\n]+/g, "");
86
86
  const json = dropUndefined(JSON.parse(sanitized));
87
87
  dbg("project file:", p, "\u2192", mask(json));
88
- return json;
88
+ return { config: json, path: p };
89
89
  };
90
90
  if (databaseId) {
91
91
  const specific = path.resolve(cwd, `onyx-database-${databaseId}.json`);
@@ -100,11 +100,11 @@ async function readProjectFile(databaseId) {
100
100
  return await tryRead(fallback);
101
101
  } catch {
102
102
  dbg("project file not found:", fallback);
103
- return {};
103
+ return { config: {} };
104
104
  }
105
105
  }
106
106
  async function readHomeProfile(databaseId) {
107
- if (!isNode) return {};
107
+ if (!isNode) return { config: {} };
108
108
  const fs = await nodeImport("node:fs/promises");
109
109
  const os = await nodeImport("node:os");
110
110
  const path = await nodeImport("node:path");
@@ -124,7 +124,7 @@ async function readHomeProfile(databaseId) {
124
124
  const sanitized = txt.replace(/[\r\n]+/g, "");
125
125
  const json = dropUndefined(JSON.parse(sanitized));
126
126
  dbg("home profile used:", p, "\u2192", mask(json));
127
- return json;
127
+ return { config: json, path: p };
128
128
  } catch (e) {
129
129
  const msg = e instanceof Error ? e.message : String(e);
130
130
  throw new OnyxConfigError(`Failed to read ${p}: ${msg}`);
@@ -143,7 +143,7 @@ async function readHomeProfile(databaseId) {
143
143
  dbg("no home-root fallback:", defaultInHome);
144
144
  if (!await fileExists(dir)) {
145
145
  dbg("~/.onyx does not exist:", dir);
146
- return {};
146
+ return { config: {} };
147
147
  }
148
148
  const files = await fs.readdir(dir).catch(() => []);
149
149
  const matches = files.filter((f) => f.startsWith("onyx-database-") && f.endsWith(".json"));
@@ -157,10 +157,10 @@ async function readHomeProfile(databaseId) {
157
157
  );
158
158
  }
159
159
  dbg("no usable home profiles found in", dir);
160
- return {};
160
+ return { config: {} };
161
161
  }
162
162
  async function readConfigPath(p) {
163
- if (!isNode) return {};
163
+ if (!isNode) return { config: {} };
164
164
  const fs = await nodeImport("node:fs/promises");
165
165
  const path = await nodeImport("node:path");
166
166
  const cwd = gProcess?.cwd?.() ?? ".";
@@ -170,7 +170,7 @@ async function readConfigPath(p) {
170
170
  const sanitized = txt.replace(/[\r\n]+/g, "");
171
171
  const json = dropUndefined(JSON.parse(sanitized));
172
172
  dbg("config path:", resolved, "\u2192", mask(json));
173
- return json;
173
+ return { config: json, path: resolved };
174
174
  } catch (e) {
175
175
  const msg = e instanceof Error ? e.message : String(e);
176
176
  throw new OnyxConfigError(`Failed to read ${resolved}: ${msg}`);
@@ -181,7 +181,8 @@ async function resolveConfig(input) {
181
181
  const env = readEnv(input?.databaseId);
182
182
  let cfgPath = {};
183
183
  if (configPath) {
184
- cfgPath = await readConfigPath(configPath);
184
+ const cfgRes = await readConfigPath(configPath);
185
+ cfgPath = cfgRes.config;
185
186
  }
186
187
  const targetId = input?.databaseId ?? env.databaseId ?? cfgPath.databaseId;
187
188
  let haveDbId = !!(input?.databaseId ?? env.databaseId ?? cfgPath.databaseId);
@@ -189,14 +190,16 @@ async function resolveConfig(input) {
189
190
  let haveApiSecret = !!(input?.apiSecret ?? env.apiSecret ?? cfgPath.apiSecret);
190
191
  let project = {};
191
192
  if (!(haveDbId && haveApiKey && haveApiSecret)) {
192
- project = await readProjectFile(targetId);
193
+ const projRes = await readProjectFile(targetId);
194
+ project = projRes.config;
193
195
  if (project.databaseId) haveDbId = true;
194
196
  if (project.apiKey) haveApiKey = true;
195
197
  if (project.apiSecret) haveApiSecret = true;
196
198
  }
197
199
  let home = {};
198
200
  if (!(haveDbId && haveApiKey && haveApiSecret)) {
199
- home = await readHomeProfile(targetId);
201
+ const homeRes = await readHomeProfile(targetId);
202
+ home = homeRes.config;
200
203
  }
201
204
  const merged = {
202
205
  baseUrl: DEFAULT_BASE_URL,
@@ -1055,6 +1058,194 @@ var OnyxError = class extends Error {
1055
1058
  }
1056
1059
  };
1057
1060
 
1061
+ // src/helpers/schema-diff.ts
1062
+ function mapByName(items) {
1063
+ const map = /* @__PURE__ */ new Map();
1064
+ for (const item of items ?? []) {
1065
+ if (!item?.name) continue;
1066
+ map.set(item.name, item);
1067
+ }
1068
+ return map;
1069
+ }
1070
+ function normalizeEntities(schema) {
1071
+ if (Array.isArray(schema.entities)) {
1072
+ return schema.entities ?? [];
1073
+ }
1074
+ const tables = schema.tables;
1075
+ if (!Array.isArray(tables)) return [];
1076
+ return tables.map((table) => ({
1077
+ name: table.name,
1078
+ attributes: table.attributes ?? []
1079
+ }));
1080
+ }
1081
+ function normalizePartition(partition) {
1082
+ if (partition == null) return "";
1083
+ const trimmed = partition.trim();
1084
+ return trimmed;
1085
+ }
1086
+ function identifiersEqual(a, b) {
1087
+ if (!a && !b) return true;
1088
+ if (!a || !b) return false;
1089
+ return a.name === b.name && a.generator === b.generator && a.type === b.type;
1090
+ }
1091
+ function diffAttributes(apiAttrs, localAttrs) {
1092
+ const apiMap = mapByName(apiAttrs);
1093
+ const localMap = mapByName(localAttrs);
1094
+ const added = [];
1095
+ const removed = [];
1096
+ const changed = [];
1097
+ for (const [name, local] of localMap.entries()) {
1098
+ if (!apiMap.has(name)) {
1099
+ added.push(local);
1100
+ continue;
1101
+ }
1102
+ const api = apiMap.get(name);
1103
+ const apiNull = Boolean(api.isNullable);
1104
+ const localNull = Boolean(local.isNullable);
1105
+ if (api.type !== local.type || apiNull !== localNull) {
1106
+ changed.push({
1107
+ name,
1108
+ from: { type: api.type, isNullable: apiNull },
1109
+ to: { type: local.type, isNullable: localNull }
1110
+ });
1111
+ }
1112
+ }
1113
+ for (const name of apiMap.keys()) {
1114
+ if (!localMap.has(name)) removed.push(name);
1115
+ }
1116
+ added.sort((a, b) => a.name.localeCompare(b.name));
1117
+ removed.sort();
1118
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1119
+ if (!added.length && !removed.length && !changed.length) return null;
1120
+ return { added, removed, changed };
1121
+ }
1122
+ function diffIndexes(apiIndexes, localIndexes) {
1123
+ const apiMap = mapByName(apiIndexes);
1124
+ const localMap = mapByName(localIndexes);
1125
+ const added = [];
1126
+ const removed = [];
1127
+ const changed = [];
1128
+ for (const [name, local] of localMap.entries()) {
1129
+ if (!apiMap.has(name)) {
1130
+ added.push(local);
1131
+ continue;
1132
+ }
1133
+ const api = apiMap.get(name);
1134
+ const apiType = api.type ?? "DEFAULT";
1135
+ const localType = local.type ?? "DEFAULT";
1136
+ const apiScore = api.minimumScore;
1137
+ const localScore = local.minimumScore;
1138
+ if (apiType !== localType || apiScore !== localScore) {
1139
+ changed.push({ name, from: api, to: local });
1140
+ }
1141
+ }
1142
+ for (const name of apiMap.keys()) {
1143
+ if (!localMap.has(name)) removed.push(name);
1144
+ }
1145
+ added.sort((a, b) => a.name.localeCompare(b.name));
1146
+ removed.sort();
1147
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1148
+ if (!added.length && !removed.length && !changed.length) return null;
1149
+ return { added, removed, changed };
1150
+ }
1151
+ function diffResolvers(apiResolvers, localResolvers) {
1152
+ const apiMap = mapByName(apiResolvers);
1153
+ const localMap = mapByName(localResolvers);
1154
+ const added = [];
1155
+ const removed = [];
1156
+ const changed = [];
1157
+ for (const [name, local] of localMap.entries()) {
1158
+ if (!apiMap.has(name)) {
1159
+ added.push(local);
1160
+ continue;
1161
+ }
1162
+ const api = apiMap.get(name);
1163
+ if (api.resolver !== local.resolver) {
1164
+ changed.push({ name, from: api, to: local });
1165
+ }
1166
+ }
1167
+ for (const name of apiMap.keys()) {
1168
+ if (!localMap.has(name)) removed.push(name);
1169
+ }
1170
+ added.sort((a, b) => a.name.localeCompare(b.name));
1171
+ removed.sort();
1172
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1173
+ if (!added.length && !removed.length && !changed.length) return null;
1174
+ return { added, removed, changed };
1175
+ }
1176
+ function diffTriggers(apiTriggers, localTriggers) {
1177
+ const apiMap = mapByName(apiTriggers);
1178
+ const localMap = mapByName(localTriggers);
1179
+ const added = [];
1180
+ const removed = [];
1181
+ const changed = [];
1182
+ for (const [name, local] of localMap.entries()) {
1183
+ if (!apiMap.has(name)) {
1184
+ added.push(local);
1185
+ continue;
1186
+ }
1187
+ const api = apiMap.get(name);
1188
+ if (api.event !== local.event || api.trigger !== local.trigger) {
1189
+ changed.push({ name, from: api, to: local });
1190
+ }
1191
+ }
1192
+ for (const name of apiMap.keys()) {
1193
+ if (!localMap.has(name)) removed.push(name);
1194
+ }
1195
+ added.sort((a, b) => a.name.localeCompare(b.name));
1196
+ removed.sort();
1197
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1198
+ if (!added.length && !removed.length && !changed.length) return null;
1199
+ return { added, removed, changed };
1200
+ }
1201
+ function computeSchemaDiff(apiSchema, localSchema) {
1202
+ const apiEntities = normalizeEntities(apiSchema);
1203
+ const localEntities = normalizeEntities(localSchema);
1204
+ const apiMap = mapByName(apiEntities);
1205
+ const localMap = mapByName(localEntities);
1206
+ const newTables = [];
1207
+ const removedTables = [];
1208
+ const changedTables = [];
1209
+ for (const [name, localEntity] of localMap.entries()) {
1210
+ if (!apiMap.has(name)) {
1211
+ newTables.push(name);
1212
+ continue;
1213
+ }
1214
+ const apiEntity = apiMap.get(name);
1215
+ const tableDiff = { name };
1216
+ const partitionFrom = normalizePartition(apiEntity.partition);
1217
+ const partitionTo = normalizePartition(localEntity.partition);
1218
+ if (partitionFrom !== partitionTo) {
1219
+ tableDiff.partition = { from: partitionFrom || null, to: partitionTo || null };
1220
+ }
1221
+ if (!identifiersEqual(apiEntity.identifier, localEntity.identifier)) {
1222
+ tableDiff.identifier = {
1223
+ from: apiEntity.identifier ?? null,
1224
+ to: localEntity.identifier ?? null
1225
+ };
1226
+ }
1227
+ const attrs = diffAttributes(apiEntity.attributes, localEntity.attributes);
1228
+ if (attrs) tableDiff.attributes = attrs;
1229
+ const indexes = diffIndexes(apiEntity.indexes, localEntity.indexes);
1230
+ if (indexes) tableDiff.indexes = indexes;
1231
+ const resolvers = diffResolvers(apiEntity.resolvers, localEntity.resolvers);
1232
+ if (resolvers) tableDiff.resolvers = resolvers;
1233
+ const triggers = diffTriggers(apiEntity.triggers, localEntity.triggers);
1234
+ if (triggers) tableDiff.triggers = triggers;
1235
+ const hasChange = tableDiff.partition || tableDiff.identifier || tableDiff.attributes || tableDiff.indexes || tableDiff.resolvers || tableDiff.triggers;
1236
+ if (hasChange) {
1237
+ changedTables.push(tableDiff);
1238
+ }
1239
+ }
1240
+ for (const name of apiMap.keys()) {
1241
+ if (!localMap.has(name)) removedTables.push(name);
1242
+ }
1243
+ newTables.sort();
1244
+ removedTables.sort();
1245
+ changedTables.sort((a, b) => a.name.localeCompare(b.name));
1246
+ return { newTables, removedTables, changedTables };
1247
+ }
1248
+
1058
1249
  // src/impl/onyx.ts
1059
1250
  var DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
1060
1251
  var cachedCfg = null;
@@ -1125,7 +1316,20 @@ function normalizeDate(value) {
1125
1316
  return Number.isNaN(ts.getTime()) ? void 0 : ts;
1126
1317
  }
1127
1318
  function normalizeSchemaRevision(input, fallbackDatabaseId) {
1128
- const { meta, createdAt, publishedAt, revisionId, entityText, ...rest } = input;
1319
+ const {
1320
+ meta,
1321
+ createdAt,
1322
+ publishedAt,
1323
+ revisionId,
1324
+ entityText,
1325
+ databaseId,
1326
+ entities,
1327
+ revisionDescription,
1328
+ ...rest
1329
+ } = input;
1330
+ const dbId = typeof databaseId === "string" ? databaseId : fallbackDatabaseId;
1331
+ const entityList = Array.isArray(entities) ? entities : [];
1332
+ const revisionDesc = typeof revisionDescription === "string" ? revisionDescription : void 0;
1129
1333
  const mergedMeta = {
1130
1334
  revisionId: meta?.revisionId ?? revisionId,
1131
1335
  createdAt: normalizeDate(meta?.createdAt ?? createdAt),
@@ -1133,10 +1337,11 @@ function normalizeSchemaRevision(input, fallbackDatabaseId) {
1133
1337
  };
1134
1338
  const cleanedMeta = mergedMeta.revisionId || mergedMeta.createdAt || mergedMeta.publishedAt ? mergedMeta : void 0;
1135
1339
  return {
1136
- ...rest,
1137
- databaseId: input.databaseId ?? fallbackDatabaseId,
1340
+ databaseId: dbId,
1341
+ revisionDescription: revisionDesc,
1342
+ entities: entityList,
1138
1343
  meta: cleanedMeta,
1139
- entities: input.entities ?? []
1344
+ ...rest
1140
1345
  };
1141
1346
  }
1142
1347
  var OnyxDatabaseImpl = class {
@@ -1248,7 +1453,8 @@ var OnyxDatabaseImpl = class {
1248
1453
  const path = `/data/${encodeURIComponent(databaseId)}/${encodeURIComponent(
1249
1454
  table
1250
1455
  )}/${encodeURIComponent(primaryKey)}${params.toString() ? `?${params.toString()}` : ""}`;
1251
- return http.request("DELETE", path);
1456
+ await http.request("DELETE", path);
1457
+ return true;
1252
1458
  }
1253
1459
  async saveDocument(doc) {
1254
1460
  const { http, databaseId } = await this.ensureClient();
@@ -1291,6 +1497,10 @@ var OnyxDatabaseImpl = class {
1291
1497
  const res = await http.request("GET", path);
1292
1498
  return Array.isArray(res) ? res.map((entry) => normalizeSchemaRevision(entry, databaseId)) : [];
1293
1499
  }
1500
+ async diffSchema(localSchema) {
1501
+ const apiSchema = await this.getSchema();
1502
+ return computeSchemaDiff(apiSchema, localSchema);
1503
+ }
1294
1504
  async updateSchema(schema, options) {
1295
1505
  const { http, databaseId } = await this.ensureClient();
1296
1506
  const params = new URLSearchParams();