@onyx.dev/onyx-database 1.0.0 → 1.0.3

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
@@ -69,7 +69,7 @@ function readEnv(targetId) {
69
69
  return res;
70
70
  }
71
71
  async function readProjectFile(databaseId) {
72
- if (!isNode) return {};
72
+ if (!isNode) return { config: {} };
73
73
  const fs = await nodeImport("node:fs/promises");
74
74
  const path = await nodeImport("node:path");
75
75
  const cwd = gProcess?.cwd?.() ?? ".";
@@ -78,7 +78,7 @@ async function readProjectFile(databaseId) {
78
78
  const sanitized = txt.replace(/[\r\n]+/g, "");
79
79
  const json = dropUndefined(JSON.parse(sanitized));
80
80
  dbg("project file:", p, "\u2192", mask(json));
81
- return json;
81
+ return { config: json, path: p };
82
82
  };
83
83
  if (databaseId) {
84
84
  const specific = path.resolve(cwd, `onyx-database-${databaseId}.json`);
@@ -93,11 +93,11 @@ async function readProjectFile(databaseId) {
93
93
  return await tryRead(fallback);
94
94
  } catch {
95
95
  dbg("project file not found:", fallback);
96
- return {};
96
+ return { config: {} };
97
97
  }
98
98
  }
99
99
  async function readHomeProfile(databaseId) {
100
- if (!isNode) return {};
100
+ if (!isNode) return { config: {} };
101
101
  const fs = await nodeImport("node:fs/promises");
102
102
  const os = await nodeImport("node:os");
103
103
  const path = await nodeImport("node:path");
@@ -117,7 +117,7 @@ async function readHomeProfile(databaseId) {
117
117
  const sanitized = txt.replace(/[\r\n]+/g, "");
118
118
  const json = dropUndefined(JSON.parse(sanitized));
119
119
  dbg("home profile used:", p, "\u2192", mask(json));
120
- return json;
120
+ return { config: json, path: p };
121
121
  } catch (e) {
122
122
  const msg = e instanceof Error ? e.message : String(e);
123
123
  throw new OnyxConfigError(`Failed to read ${p}: ${msg}`);
@@ -136,7 +136,7 @@ async function readHomeProfile(databaseId) {
136
136
  dbg("no home-root fallback:", defaultInHome);
137
137
  if (!await fileExists(dir)) {
138
138
  dbg("~/.onyx does not exist:", dir);
139
- return {};
139
+ return { config: {} };
140
140
  }
141
141
  const files = await fs.readdir(dir).catch(() => []);
142
142
  const matches2 = files.filter((f) => f.startsWith("onyx-database-") && f.endsWith(".json"));
@@ -150,10 +150,10 @@ async function readHomeProfile(databaseId) {
150
150
  );
151
151
  }
152
152
  dbg("no usable home profiles found in", dir);
153
- return {};
153
+ return { config: {} };
154
154
  }
155
155
  async function readConfigPath(p) {
156
- if (!isNode) return {};
156
+ if (!isNode) return { config: {} };
157
157
  const fs = await nodeImport("node:fs/promises");
158
158
  const path = await nodeImport("node:path");
159
159
  const cwd = gProcess?.cwd?.() ?? ".";
@@ -163,7 +163,7 @@ async function readConfigPath(p) {
163
163
  const sanitized = txt.replace(/[\r\n]+/g, "");
164
164
  const json = dropUndefined(JSON.parse(sanitized));
165
165
  dbg("config path:", resolved, "\u2192", mask(json));
166
- return json;
166
+ return { config: json, path: resolved };
167
167
  } catch (e) {
168
168
  const msg = e instanceof Error ? e.message : String(e);
169
169
  throw new OnyxConfigError(`Failed to read ${resolved}: ${msg}`);
@@ -174,7 +174,8 @@ async function resolveConfig(input) {
174
174
  const env = readEnv(input?.databaseId);
175
175
  let cfgPath = {};
176
176
  if (configPath) {
177
- cfgPath = await readConfigPath(configPath);
177
+ const cfgRes = await readConfigPath(configPath);
178
+ cfgPath = cfgRes.config;
178
179
  }
179
180
  const targetId = input?.databaseId ?? env.databaseId ?? cfgPath.databaseId;
180
181
  let haveDbId = !!(input?.databaseId ?? env.databaseId ?? cfgPath.databaseId);
@@ -182,14 +183,16 @@ async function resolveConfig(input) {
182
183
  let haveApiSecret = !!(input?.apiSecret ?? env.apiSecret ?? cfgPath.apiSecret);
183
184
  let project = {};
184
185
  if (!(haveDbId && haveApiKey && haveApiSecret)) {
185
- project = await readProjectFile(targetId);
186
+ const projRes = await readProjectFile(targetId);
187
+ project = projRes.config;
186
188
  if (project.databaseId) haveDbId = true;
187
189
  if (project.apiKey) haveApiKey = true;
188
190
  if (project.apiSecret) haveApiSecret = true;
189
191
  }
190
192
  let home = {};
191
193
  if (!(haveDbId && haveApiKey && haveApiSecret)) {
192
- home = await readHomeProfile(targetId);
194
+ const homeRes = await readHomeProfile(targetId);
195
+ home = homeRes.config;
193
196
  }
194
197
  const merged = {
195
198
  baseUrl: DEFAULT_BASE_URL,
@@ -208,6 +211,10 @@ async function resolveConfig(input) {
208
211
  const fetchImpl = merged.fetch ?? (typeof gfetch === "function" ? (u, i) => gfetch(u, i) : async () => {
209
212
  throw new OnyxConfigError("No fetch available; provide OnyxConfig.fetch");
210
213
  });
214
+ const retryConfig = input?.retry ?? env.retry ?? cfgPath.retry ?? project.retry ?? home.retry ?? {};
215
+ const retryEnabled = retryConfig.enabled ?? true;
216
+ const maxRetries = retryConfig.maxRetries ?? 3;
217
+ const retryInitialDelayMs = retryConfig.initialDelayMs ?? 300;
211
218
  const missing = [];
212
219
  if (!databaseId) missing.push("databaseId");
213
220
  if (!apiKey) missing.push("apiKey");
@@ -230,7 +237,16 @@ async function resolveConfig(input) {
230
237
  `Missing required config: ${missing.join(", ")}. Sources: ${sources.join(", ")}`
231
238
  );
232
239
  }
233
- const resolved = { baseUrl, databaseId, apiKey, apiSecret, fetch: fetchImpl };
240
+ const resolved = {
241
+ baseUrl,
242
+ databaseId,
243
+ apiKey,
244
+ apiSecret,
245
+ fetch: fetchImpl,
246
+ retryEnabled,
247
+ maxRetries,
248
+ retryInitialDelayMs
249
+ };
234
250
  const source = {
235
251
  databaseId: input?.databaseId ? "explicit config" : env.databaseId ? "env" : cfgPath.databaseId ? "env ONYX_CONFIG_PATH" : project.databaseId ? "project file" : home.databaseId ? "home profile" : "unknown",
236
252
  apiKey: input?.apiKey ? "explicit config" : env.apiKey ? "env" : cfgPath.apiKey ? "env ONYX_CONFIG_PATH" : project.apiKey ? "project file" : home.apiKey ? "home profile" : "unknown",
@@ -287,7 +303,7 @@ function parseJsonAllowNaN(txt) {
287
303
  return JSON.parse(fixed);
288
304
  }
289
305
  }
290
- var HttpClient = class {
306
+ var HttpClient = class _HttpClient {
291
307
  baseUrl;
292
308
  apiKey;
293
309
  apiSecret;
@@ -295,6 +311,25 @@ var HttpClient = class {
295
311
  defaults;
296
312
  requestLoggingEnabled;
297
313
  responseLoggingEnabled;
314
+ retryEnabled;
315
+ maxRetries;
316
+ retryInitialDelayMs;
317
+ shouldRetry;
318
+ static parseRetryAfter(header) {
319
+ if (!header) return null;
320
+ const trimmed = header.trim();
321
+ if (trimmed === "") return null;
322
+ const seconds = Number(trimmed);
323
+ if (Number.isFinite(seconds)) {
324
+ return Math.max(0, seconds * 1e3);
325
+ }
326
+ const dateMs = Date.parse(trimmed);
327
+ if (!Number.isNaN(dateMs)) {
328
+ const now = Date.now();
329
+ return Math.max(0, dateMs - now);
330
+ }
331
+ return null;
332
+ }
298
333
  constructor(opts) {
299
334
  if (!opts.baseUrl || opts.baseUrl.trim() === "") {
300
335
  throw new OnyxConfigError("baseUrl is required");
@@ -319,6 +354,10 @@ var HttpClient = class {
319
354
  const envDebug = globalThis.process?.env?.ONYX_DEBUG === "true";
320
355
  this.requestLoggingEnabled = !!opts.requestLoggingEnabled || envDebug;
321
356
  this.responseLoggingEnabled = !!opts.responseLoggingEnabled || envDebug;
357
+ this.retryEnabled = opts.retryEnabled ?? true;
358
+ this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
359
+ this.retryInitialDelayMs = Math.max(0, opts.retryInitialDelayMs ?? 100);
360
+ this.shouldRetry = (method, path) => method === "GET" || path.startsWith("/query/");
322
361
  }
323
362
  headers(extra) {
324
363
  const extras = { ...extra ?? {} };
@@ -359,9 +398,8 @@ var HttpClient = class {
359
398
  headers,
360
399
  body: payload
361
400
  };
362
- const isQuery = path.includes("/query/") && !/\/query\/(?:update|delete)\//.test(path);
363
- const canRetry = method === "GET" || isQuery;
364
- const maxAttempts = canRetry ? 3 : 1;
401
+ const canRetry = this.retryEnabled && this.shouldRetry(method, path);
402
+ const maxAttempts = canRetry ? this.maxRetries + 1 : 1;
365
403
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
366
404
  try {
367
405
  const res = await this.fetchImpl(url, init);
@@ -379,7 +417,10 @@ var HttpClient = class {
379
417
  if (!res.ok) {
380
418
  const msg = typeof data === "object" && data !== null && "error" in data && typeof data.error?.message === "string" ? String(data.error.message) : `${res.status} ${res.statusText}`;
381
419
  if (canRetry && res.status >= 500 && attempt + 1 < maxAttempts) {
382
- await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
420
+ const serverRetry = _HttpClient.parseRetryAfter(res.headers.get("retry-after"));
421
+ const backoff = this.retryInitialDelayMs * 2 ** attempt;
422
+ const delay = serverRetry ?? backoff;
423
+ await new Promise((r) => setTimeout(r, delay));
383
424
  continue;
384
425
  }
385
426
  throw new OnyxHttpError(msg, res.status, res.statusText, data, raw);
@@ -388,7 +429,8 @@ var HttpClient = class {
388
429
  } catch (err) {
389
430
  const retryable = canRetry && (!(err instanceof OnyxHttpError) || err.status >= 500);
390
431
  if (attempt + 1 < maxAttempts && retryable) {
391
- await new Promise((r) => setTimeout(r, 100 * 2 ** attempt));
432
+ const delay = this.retryInitialDelayMs * 2 ** attempt;
433
+ await new Promise((r) => setTimeout(r, delay));
392
434
  continue;
393
435
  }
394
436
  throw err;
@@ -949,31 +991,196 @@ var CascadeRelationshipBuilder = class {
949
991
  }
950
992
  };
951
993
 
952
- // src/errors/onyx-error.ts
953
- var OnyxError = class extends Error {
954
- name = "OnyxError";
955
- constructor(message) {
956
- super(message);
994
+ // src/helpers/schema-diff.ts
995
+ function mapByName(items) {
996
+ const map = /* @__PURE__ */ new Map();
997
+ for (const item of items ?? []) {
998
+ if (!item?.name) continue;
999
+ map.set(item.name, item);
1000
+ }
1001
+ return map;
1002
+ }
1003
+ function normalizeEntities(schema) {
1004
+ if (Array.isArray(schema.entities)) {
1005
+ return schema.entities ?? [];
1006
+ }
1007
+ const tables = schema.tables;
1008
+ if (!Array.isArray(tables)) return [];
1009
+ return tables.map((table) => ({
1010
+ name: table.name,
1011
+ attributes: table.attributes ?? []
1012
+ }));
1013
+ }
1014
+ function normalizePartition(partition) {
1015
+ if (partition == null) return "";
1016
+ const trimmed = partition.trim();
1017
+ return trimmed;
1018
+ }
1019
+ function identifiersEqual(a, b) {
1020
+ if (!a && !b) return true;
1021
+ if (!a || !b) return false;
1022
+ return a.name === b.name && a.generator === b.generator && a.type === b.type;
1023
+ }
1024
+ function diffAttributes(apiAttrs, localAttrs) {
1025
+ const apiMap = mapByName(apiAttrs);
1026
+ const localMap = mapByName(localAttrs);
1027
+ const added = [];
1028
+ const removed = [];
1029
+ const changed = [];
1030
+ for (const [name, local] of localMap.entries()) {
1031
+ if (!apiMap.has(name)) {
1032
+ added.push(local);
1033
+ continue;
1034
+ }
1035
+ const api = apiMap.get(name);
1036
+ const apiNull = Boolean(api.isNullable);
1037
+ const localNull = Boolean(local.isNullable);
1038
+ if (api.type !== local.type || apiNull !== localNull) {
1039
+ changed.push({
1040
+ name,
1041
+ from: { type: api.type, isNullable: apiNull },
1042
+ to: { type: local.type, isNullable: localNull }
1043
+ });
1044
+ }
957
1045
  }
958
- };
959
-
960
- // src/impl/onyx.ts
961
- var DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
962
- var cachedCfg = null;
963
- function resolveConfigWithCache(config) {
964
- const ttl = config?.ttl ?? DEFAULT_CACHE_TTL;
965
- const now = Date.now();
966
- if (cachedCfg && cachedCfg.expires > now) {
967
- return cachedCfg.promise;
968
- }
969
- const { ttl: _ttl, requestLoggingEnabled: _reqLog, responseLoggingEnabled: _resLog, ...rest } = config ?? {};
970
- const promise = resolveConfig(rest);
971
- cachedCfg = { promise, expires: now + ttl };
972
- return promise;
1046
+ for (const name of apiMap.keys()) {
1047
+ if (!localMap.has(name)) removed.push(name);
1048
+ }
1049
+ added.sort((a, b) => a.name.localeCompare(b.name));
1050
+ removed.sort();
1051
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1052
+ if (!added.length && !removed.length && !changed.length) return null;
1053
+ return { added, removed, changed };
973
1054
  }
974
- function clearCacheConfig() {
975
- cachedCfg = null;
1055
+ function diffIndexes(apiIndexes, localIndexes) {
1056
+ const apiMap = mapByName(apiIndexes);
1057
+ const localMap = mapByName(localIndexes);
1058
+ const added = [];
1059
+ const removed = [];
1060
+ const changed = [];
1061
+ for (const [name, local] of localMap.entries()) {
1062
+ if (!apiMap.has(name)) {
1063
+ added.push(local);
1064
+ continue;
1065
+ }
1066
+ const api = apiMap.get(name);
1067
+ const apiType = api.type ?? "DEFAULT";
1068
+ const localType = local.type ?? "DEFAULT";
1069
+ const apiScore = api.minimumScore;
1070
+ const localScore = local.minimumScore;
1071
+ if (apiType !== localType || apiScore !== localScore) {
1072
+ changed.push({ name, from: api, to: local });
1073
+ }
1074
+ }
1075
+ for (const name of apiMap.keys()) {
1076
+ if (!localMap.has(name)) removed.push(name);
1077
+ }
1078
+ added.sort((a, b) => a.name.localeCompare(b.name));
1079
+ removed.sort();
1080
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1081
+ if (!added.length && !removed.length && !changed.length) return null;
1082
+ return { added, removed, changed };
1083
+ }
1084
+ function diffResolvers(apiResolvers, localResolvers) {
1085
+ const apiMap = mapByName(apiResolvers);
1086
+ const localMap = mapByName(localResolvers);
1087
+ const added = [];
1088
+ const removed = [];
1089
+ const changed = [];
1090
+ for (const [name, local] of localMap.entries()) {
1091
+ if (!apiMap.has(name)) {
1092
+ added.push(local);
1093
+ continue;
1094
+ }
1095
+ const api = apiMap.get(name);
1096
+ if (api.resolver !== local.resolver) {
1097
+ changed.push({ name, from: api, to: local });
1098
+ }
1099
+ }
1100
+ for (const name of apiMap.keys()) {
1101
+ if (!localMap.has(name)) removed.push(name);
1102
+ }
1103
+ added.sort((a, b) => a.name.localeCompare(b.name));
1104
+ removed.sort();
1105
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1106
+ if (!added.length && !removed.length && !changed.length) return null;
1107
+ return { added, removed, changed };
976
1108
  }
1109
+ function diffTriggers(apiTriggers, localTriggers) {
1110
+ const apiMap = mapByName(apiTriggers);
1111
+ const localMap = mapByName(localTriggers);
1112
+ const added = [];
1113
+ const removed = [];
1114
+ const changed = [];
1115
+ for (const [name, local] of localMap.entries()) {
1116
+ if (!apiMap.has(name)) {
1117
+ added.push(local);
1118
+ continue;
1119
+ }
1120
+ const api = apiMap.get(name);
1121
+ if (api.event !== local.event || api.trigger !== local.trigger) {
1122
+ changed.push({ name, from: api, to: local });
1123
+ }
1124
+ }
1125
+ for (const name of apiMap.keys()) {
1126
+ if (!localMap.has(name)) removed.push(name);
1127
+ }
1128
+ added.sort((a, b) => a.name.localeCompare(b.name));
1129
+ removed.sort();
1130
+ changed.sort((a, b) => a.name.localeCompare(b.name));
1131
+ if (!added.length && !removed.length && !changed.length) return null;
1132
+ return { added, removed, changed };
1133
+ }
1134
+ function computeSchemaDiff(apiSchema, localSchema) {
1135
+ const apiEntities = normalizeEntities(apiSchema);
1136
+ const localEntities = normalizeEntities(localSchema);
1137
+ const apiMap = mapByName(apiEntities);
1138
+ const localMap = mapByName(localEntities);
1139
+ const newTables = [];
1140
+ const removedTables = [];
1141
+ const changedTables = [];
1142
+ for (const [name, localEntity] of localMap.entries()) {
1143
+ if (!apiMap.has(name)) {
1144
+ newTables.push(name);
1145
+ continue;
1146
+ }
1147
+ const apiEntity = apiMap.get(name);
1148
+ const tableDiff = { name };
1149
+ const partitionFrom = normalizePartition(apiEntity.partition);
1150
+ const partitionTo = normalizePartition(localEntity.partition);
1151
+ if (partitionFrom !== partitionTo) {
1152
+ tableDiff.partition = { from: partitionFrom || null, to: partitionTo || null };
1153
+ }
1154
+ if (!identifiersEqual(apiEntity.identifier, localEntity.identifier)) {
1155
+ tableDiff.identifier = {
1156
+ from: apiEntity.identifier ?? null,
1157
+ to: localEntity.identifier ?? null
1158
+ };
1159
+ }
1160
+ const attrs = diffAttributes(apiEntity.attributes, localEntity.attributes);
1161
+ if (attrs) tableDiff.attributes = attrs;
1162
+ const indexes = diffIndexes(apiEntity.indexes, localEntity.indexes);
1163
+ if (indexes) tableDiff.indexes = indexes;
1164
+ const resolvers = diffResolvers(apiEntity.resolvers, localEntity.resolvers);
1165
+ if (resolvers) tableDiff.resolvers = resolvers;
1166
+ const triggers = diffTriggers(apiEntity.triggers, localEntity.triggers);
1167
+ if (triggers) tableDiff.triggers = triggers;
1168
+ const hasChange = tableDiff.partition || tableDiff.identifier || tableDiff.attributes || tableDiff.indexes || tableDiff.resolvers || tableDiff.triggers;
1169
+ if (hasChange) {
1170
+ changedTables.push(tableDiff);
1171
+ }
1172
+ }
1173
+ for (const name of apiMap.keys()) {
1174
+ if (!localMap.has(name)) removedTables.push(name);
1175
+ }
1176
+ newTables.sort();
1177
+ removedTables.sort();
1178
+ changedTables.sort((a, b) => a.name.localeCompare(b.name));
1179
+ return { newTables, removedTables, changedTables };
1180
+ }
1181
+
1182
+ // src/impl/onyx-core.ts
1183
+ var DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
977
1184
  function toSingleCondition(criteria) {
978
1185
  return { conditionType: "SingleCondition", criteria };
979
1186
  }
@@ -1027,7 +1234,20 @@ function normalizeDate(value) {
1027
1234
  return Number.isNaN(ts.getTime()) ? void 0 : ts;
1028
1235
  }
1029
1236
  function normalizeSchemaRevision(input, fallbackDatabaseId) {
1030
- const { meta, createdAt, publishedAt, revisionId, entityText, ...rest } = input;
1237
+ const {
1238
+ meta,
1239
+ createdAt,
1240
+ publishedAt,
1241
+ revisionId,
1242
+ entityText,
1243
+ databaseId,
1244
+ entities,
1245
+ revisionDescription,
1246
+ ...rest
1247
+ } = input;
1248
+ const dbId = typeof databaseId === "string" ? databaseId : fallbackDatabaseId;
1249
+ const entityList = Array.isArray(entities) ? entities : [];
1250
+ const revisionDesc = typeof revisionDescription === "string" ? revisionDescription : void 0;
1031
1251
  const mergedMeta = {
1032
1252
  revisionId: meta?.revisionId ?? revisionId,
1033
1253
  createdAt: normalizeDate(meta?.createdAt ?? createdAt),
@@ -1035,10 +1255,11 @@ function normalizeSchemaRevision(input, fallbackDatabaseId) {
1035
1255
  };
1036
1256
  const cleanedMeta = mergedMeta.revisionId || mergedMeta.createdAt || mergedMeta.publishedAt ? mergedMeta : void 0;
1037
1257
  return {
1038
- ...rest,
1039
- databaseId: input.databaseId ?? fallbackDatabaseId,
1258
+ databaseId: dbId,
1259
+ revisionDescription: revisionDesc,
1260
+ entities: entityList,
1040
1261
  meta: cleanedMeta,
1041
- entities: input.entities ?? []
1262
+ ...rest
1042
1263
  };
1043
1264
  }
1044
1265
  var OnyxDatabaseImpl = class {
@@ -1049,7 +1270,7 @@ var OnyxDatabaseImpl = class {
1049
1270
  requestLoggingEnabled;
1050
1271
  responseLoggingEnabled;
1051
1272
  defaultPartition;
1052
- constructor(config) {
1273
+ constructor(config, resolveConfigWithCache) {
1053
1274
  this.requestLoggingEnabled = !!config?.requestLoggingEnabled;
1054
1275
  this.responseLoggingEnabled = !!config?.responseLoggingEnabled;
1055
1276
  this.defaultPartition = config?.partition;
@@ -1066,7 +1287,10 @@ var OnyxDatabaseImpl = class {
1066
1287
  apiSecret: this.resolved.apiSecret,
1067
1288
  fetchImpl: this.resolved.fetch,
1068
1289
  requestLoggingEnabled: this.requestLoggingEnabled,
1069
- responseLoggingEnabled: this.responseLoggingEnabled
1290
+ responseLoggingEnabled: this.responseLoggingEnabled,
1291
+ retryEnabled: this.resolved.retryEnabled,
1292
+ maxRetries: this.resolved.maxRetries,
1293
+ retryInitialDelayMs: this.resolved.retryInitialDelayMs
1070
1294
  });
1071
1295
  }
1072
1296
  return {
@@ -1150,7 +1374,8 @@ var OnyxDatabaseImpl = class {
1150
1374
  const path = `/data/${encodeURIComponent(databaseId)}/${encodeURIComponent(
1151
1375
  table
1152
1376
  )}/${encodeURIComponent(primaryKey)}${params.toString() ? `?${params.toString()}` : ""}`;
1153
- return http.request("DELETE", path);
1377
+ await http.request("DELETE", path);
1378
+ return true;
1154
1379
  }
1155
1380
  async saveDocument(doc) {
1156
1381
  const { http, databaseId } = await this.ensureClient();
@@ -1193,6 +1418,10 @@ var OnyxDatabaseImpl = class {
1193
1418
  const res = await http.request("GET", path);
1194
1419
  return Array.isArray(res) ? res.map((entry) => normalizeSchemaRevision(entry, databaseId)) : [];
1195
1420
  }
1421
+ async diffSchema(localSchema) {
1422
+ const apiSchema = await this.getSchema();
1423
+ return computeSchemaDiff(apiSchema, localSchema);
1424
+ }
1196
1425
  async updateSchema(schema, options) {
1197
1426
  const { http, databaseId } = await this.ensureClient();
1198
1427
  const params = new URLSearchParams();
@@ -1526,7 +1755,6 @@ var QueryBuilderImpl = class {
1526
1755
  }
1527
1756
  async firstOrNull() {
1528
1757
  if (this.mode !== "select") throw new Error("Cannot call firstOrNull() in update mode.");
1529
- if (!this.conditions) throw new OnyxError("firstOrNull() requires a where() clause.");
1530
1758
  this.limitValue = 1;
1531
1759
  const pg = await this.page();
1532
1760
  return Array.isArray(pg.records) && pg.records.length > 0 ? pg.records[0] : null;
@@ -1618,12 +1846,32 @@ var CascadeBuilderImpl = class {
1618
1846
  return this.db.delete(table, primaryKey, opts);
1619
1847
  }
1620
1848
  };
1621
- var onyx = {
1622
- init(config) {
1623
- return new OnyxDatabaseImpl(config);
1624
- },
1625
- clearCacheConfig
1626
- };
1849
+ function createOnyxFacade(resolveConfig2) {
1850
+ let cachedCfg = null;
1851
+ function resolveConfigWithCache(config) {
1852
+ const ttl = config?.ttl ?? DEFAULT_CACHE_TTL;
1853
+ const now = Date.now();
1854
+ if (cachedCfg && cachedCfg.expires > now) {
1855
+ return cachedCfg.promise;
1856
+ }
1857
+ const { ttl: _ttl, requestLoggingEnabled: _reqLog, responseLoggingEnabled: _resLog, ...rest } = config ?? {};
1858
+ const promise = resolveConfig2(rest);
1859
+ cachedCfg = { promise, expires: now + ttl };
1860
+ return promise;
1861
+ }
1862
+ function clearCacheConfig() {
1863
+ cachedCfg = null;
1864
+ }
1865
+ return {
1866
+ init(config) {
1867
+ return new OnyxDatabaseImpl(config, resolveConfigWithCache);
1868
+ },
1869
+ clearCacheConfig
1870
+ };
1871
+ }
1872
+
1873
+ // src/impl/onyx.ts
1874
+ var onyx = createOnyxFacade((config) => resolveConfig(config));
1627
1875
 
1628
1876
  // src/helpers/sort.ts
1629
1877
  var asc = (field) => ({ field, order: "ASC" });