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