@onyx.dev/onyx-database 0.3.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 +71 -1
- package/dist/gen/cli/generate.cjs +313 -30
- package/dist/gen/cli/generate.cjs.map +1 -1
- package/dist/index.cjs +329 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +92 -6
- package/dist/index.d.ts +92 -6
- package/dist/index.js +328 -37
- package/dist/index.js.map +1 -1
- package/dist/schema/cli/schema.cjs +496 -35
- package/dist/schema/cli/schema.cjs.map +1 -1
- package/package.json +1 -1
|
@@ -78,7 +78,7 @@ function readEnv(targetId) {
|
|
|
78
78
|
return res;
|
|
79
79
|
}
|
|
80
80
|
async function readProjectFile(databaseId) {
|
|
81
|
-
if (!isNode) return {};
|
|
81
|
+
if (!isNode) return { config: {} };
|
|
82
82
|
const fs = await nodeImport("node:fs/promises");
|
|
83
83
|
const path2 = await nodeImport("node:path");
|
|
84
84
|
const cwd = gProcess?.cwd?.() ?? ".";
|
|
@@ -87,7 +87,7 @@ async function readProjectFile(databaseId) {
|
|
|
87
87
|
const sanitized = txt.replace(/[\r\n]+/g, "");
|
|
88
88
|
const json = dropUndefined(JSON.parse(sanitized));
|
|
89
89
|
dbg("project file:", p, "\u2192", mask(json));
|
|
90
|
-
return json;
|
|
90
|
+
return { config: json, path: p };
|
|
91
91
|
};
|
|
92
92
|
if (databaseId) {
|
|
93
93
|
const specific = path2.resolve(cwd, `onyx-database-${databaseId}.json`);
|
|
@@ -102,11 +102,11 @@ async function readProjectFile(databaseId) {
|
|
|
102
102
|
return await tryRead(fallback);
|
|
103
103
|
} catch {
|
|
104
104
|
dbg("project file not found:", fallback);
|
|
105
|
-
return {};
|
|
105
|
+
return { config: {} };
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
async function readHomeProfile(databaseId) {
|
|
109
|
-
if (!isNode) return {};
|
|
109
|
+
if (!isNode) return { config: {} };
|
|
110
110
|
const fs = await nodeImport("node:fs/promises");
|
|
111
111
|
const os = await nodeImport("node:os");
|
|
112
112
|
const path2 = await nodeImport("node:path");
|
|
@@ -126,7 +126,7 @@ async function readHomeProfile(databaseId) {
|
|
|
126
126
|
const sanitized = txt.replace(/[\r\n]+/g, "");
|
|
127
127
|
const json = dropUndefined(JSON.parse(sanitized));
|
|
128
128
|
dbg("home profile used:", p, "\u2192", mask(json));
|
|
129
|
-
return json;
|
|
129
|
+
return { config: json, path: p };
|
|
130
130
|
} catch (e) {
|
|
131
131
|
const msg = e instanceof Error ? e.message : String(e);
|
|
132
132
|
throw new OnyxConfigError(`Failed to read ${p}: ${msg}`);
|
|
@@ -145,7 +145,7 @@ async function readHomeProfile(databaseId) {
|
|
|
145
145
|
dbg("no home-root fallback:", defaultInHome);
|
|
146
146
|
if (!await fileExists(dir)) {
|
|
147
147
|
dbg("~/.onyx does not exist:", dir);
|
|
148
|
-
return {};
|
|
148
|
+
return { config: {} };
|
|
149
149
|
}
|
|
150
150
|
const files = await fs.readdir(dir).catch(() => []);
|
|
151
151
|
const matches = files.filter((f) => f.startsWith("onyx-database-") && f.endsWith(".json"));
|
|
@@ -159,10 +159,10 @@ async function readHomeProfile(databaseId) {
|
|
|
159
159
|
);
|
|
160
160
|
}
|
|
161
161
|
dbg("no usable home profiles found in", dir);
|
|
162
|
-
return {};
|
|
162
|
+
return { config: {} };
|
|
163
163
|
}
|
|
164
164
|
async function readConfigPath(p) {
|
|
165
|
-
if (!isNode) return {};
|
|
165
|
+
if (!isNode) return { config: {} };
|
|
166
166
|
const fs = await nodeImport("node:fs/promises");
|
|
167
167
|
const path2 = await nodeImport("node:path");
|
|
168
168
|
const cwd = gProcess?.cwd?.() ?? ".";
|
|
@@ -172,7 +172,7 @@ async function readConfigPath(p) {
|
|
|
172
172
|
const sanitized = txt.replace(/[\r\n]+/g, "");
|
|
173
173
|
const json = dropUndefined(JSON.parse(sanitized));
|
|
174
174
|
dbg("config path:", resolved, "\u2192", mask(json));
|
|
175
|
-
return json;
|
|
175
|
+
return { config: json, path: resolved };
|
|
176
176
|
} catch (e) {
|
|
177
177
|
const msg = e instanceof Error ? e.message : String(e);
|
|
178
178
|
throw new OnyxConfigError(`Failed to read ${resolved}: ${msg}`);
|
|
@@ -183,7 +183,8 @@ async function resolveConfig(input) {
|
|
|
183
183
|
const env = readEnv(input?.databaseId);
|
|
184
184
|
let cfgPath = {};
|
|
185
185
|
if (configPath) {
|
|
186
|
-
|
|
186
|
+
const cfgRes = await readConfigPath(configPath);
|
|
187
|
+
cfgPath = cfgRes.config;
|
|
187
188
|
}
|
|
188
189
|
const targetId = input?.databaseId ?? env.databaseId ?? cfgPath.databaseId;
|
|
189
190
|
let haveDbId = !!(input?.databaseId ?? env.databaseId ?? cfgPath.databaseId);
|
|
@@ -191,14 +192,16 @@ async function resolveConfig(input) {
|
|
|
191
192
|
let haveApiSecret = !!(input?.apiSecret ?? env.apiSecret ?? cfgPath.apiSecret);
|
|
192
193
|
let project = {};
|
|
193
194
|
if (!(haveDbId && haveApiKey && haveApiSecret)) {
|
|
194
|
-
|
|
195
|
+
const projRes = await readProjectFile(targetId);
|
|
196
|
+
project = projRes.config;
|
|
195
197
|
if (project.databaseId) haveDbId = true;
|
|
196
198
|
if (project.apiKey) haveApiKey = true;
|
|
197
199
|
if (project.apiSecret) haveApiSecret = true;
|
|
198
200
|
}
|
|
199
201
|
let home = {};
|
|
200
202
|
if (!(haveDbId && haveApiKey && haveApiSecret)) {
|
|
201
|
-
|
|
203
|
+
const homeRes = await readHomeProfile(targetId);
|
|
204
|
+
home = homeRes.config;
|
|
202
205
|
}
|
|
203
206
|
const merged = {
|
|
204
207
|
baseUrl: DEFAULT_BASE_URL,
|
|
@@ -249,6 +252,27 @@ async function resolveConfig(input) {
|
|
|
249
252
|
dbg("resolved:", mask(resolved));
|
|
250
253
|
return resolved;
|
|
251
254
|
}
|
|
255
|
+
async function resolveConfigWithSource(input) {
|
|
256
|
+
const configPathEnv = gProcess?.env?.ONYX_CONFIG_PATH;
|
|
257
|
+
const env = readEnv(input?.databaseId);
|
|
258
|
+
const cfgPathRes = configPathEnv ? await readConfigPath(configPathEnv) : { config: {} };
|
|
259
|
+
const cfgPath = cfgPathRes.config;
|
|
260
|
+
const projectRes = await readProjectFile(env.databaseId ?? cfgPath.databaseId);
|
|
261
|
+
const project = projectRes.config;
|
|
262
|
+
const homeRes = await readHomeProfile(env.databaseId ?? cfgPath.databaseId);
|
|
263
|
+
const home = homeRes.config;
|
|
264
|
+
const base = await resolveConfig(input);
|
|
265
|
+
const sources = {
|
|
266
|
+
baseUrl: input?.baseUrl ? "explicit config" : env.baseUrl ? "env" : cfgPath.baseUrl ? "env ONYX_CONFIG_PATH" : project.baseUrl ? "project file" : home.baseUrl ? "home profile" : "default",
|
|
267
|
+
databaseId: input?.databaseId ? "explicit config" : env.databaseId ? "env" : cfgPath.databaseId ? "env ONYX_CONFIG_PATH" : project.databaseId ? "project file" : home.databaseId ? "home profile" : "unknown",
|
|
268
|
+
apiKey: input?.apiKey ? "explicit config" : env.apiKey ? "env" : cfgPath.apiKey ? "env ONYX_CONFIG_PATH" : project.apiKey ? "project file" : home.apiKey ? "home profile" : "unknown",
|
|
269
|
+
apiSecret: input?.apiSecret ? "explicit config" : env.apiSecret ? "env" : cfgPath.apiSecret ? "env ONYX_CONFIG_PATH" : project.apiSecret ? "project file" : home.apiSecret ? "home profile" : "unknown",
|
|
270
|
+
configPath: cfgPathRes.path,
|
|
271
|
+
projectFile: projectRes.path,
|
|
272
|
+
homeProfile: homeRes.path
|
|
273
|
+
};
|
|
274
|
+
return { ...base, sources };
|
|
275
|
+
}
|
|
252
276
|
function mask(obj) {
|
|
253
277
|
if (!obj) return obj;
|
|
254
278
|
const clone = { ...obj };
|
|
@@ -842,6 +866,56 @@ var QueryResults = class extends Array {
|
|
|
842
866
|
}
|
|
843
867
|
};
|
|
844
868
|
|
|
869
|
+
// src/helpers/condition-normalizer.ts
|
|
870
|
+
function isQueryBuilderLike(value) {
|
|
871
|
+
return !!value && typeof value.toSerializableQueryObject === "function";
|
|
872
|
+
}
|
|
873
|
+
function normalizeCriteriaValue(value) {
|
|
874
|
+
if (Array.isArray(value)) {
|
|
875
|
+
let changed = false;
|
|
876
|
+
const normalized = value.map((item) => {
|
|
877
|
+
const result = normalizeCriteriaValue(item);
|
|
878
|
+
if (result.changed) changed = true;
|
|
879
|
+
return result.value;
|
|
880
|
+
});
|
|
881
|
+
if (!changed) {
|
|
882
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
883
|
+
if (normalized[i] !== value[i]) {
|
|
884
|
+
changed = true;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return { value: changed ? normalized : value, changed };
|
|
890
|
+
}
|
|
891
|
+
if (isQueryBuilderLike(value)) {
|
|
892
|
+
return { value: value.toSerializableQueryObject(), changed: true };
|
|
893
|
+
}
|
|
894
|
+
return { value, changed: false };
|
|
895
|
+
}
|
|
896
|
+
function normalizeConditionInternal(condition) {
|
|
897
|
+
if (condition.conditionType === "SingleCondition") {
|
|
898
|
+
const { value, changed: changed2 } = normalizeCriteriaValue(condition.criteria.value);
|
|
899
|
+
if (!changed2) return condition;
|
|
900
|
+
return {
|
|
901
|
+
...condition,
|
|
902
|
+
criteria: { ...condition.criteria, value }
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
let changed = false;
|
|
906
|
+
const normalizedConditions = condition.conditions.map((child) => {
|
|
907
|
+
const normalized = normalizeConditionInternal(child);
|
|
908
|
+
if (normalized !== child) changed = true;
|
|
909
|
+
return normalized;
|
|
910
|
+
});
|
|
911
|
+
if (!changed) return condition;
|
|
912
|
+
return { ...condition, conditions: normalizedConditions };
|
|
913
|
+
}
|
|
914
|
+
function normalizeCondition(condition) {
|
|
915
|
+
if (!condition) return condition;
|
|
916
|
+
return normalizeConditionInternal(condition);
|
|
917
|
+
}
|
|
918
|
+
|
|
845
919
|
// src/builders/cascade-relationship-builder.ts
|
|
846
920
|
var CascadeRelationshipBuilder = class {
|
|
847
921
|
graphName;
|
|
@@ -916,6 +990,297 @@ var OnyxError = class extends Error {
|
|
|
916
990
|
}
|
|
917
991
|
};
|
|
918
992
|
|
|
993
|
+
// src/helpers/schema-diff.ts
|
|
994
|
+
function mapByName(items) {
|
|
995
|
+
const map = /* @__PURE__ */ new Map();
|
|
996
|
+
for (const item of items ?? []) {
|
|
997
|
+
if (!item?.name) continue;
|
|
998
|
+
map.set(item.name, item);
|
|
999
|
+
}
|
|
1000
|
+
return map;
|
|
1001
|
+
}
|
|
1002
|
+
function normalizeEntities(schema) {
|
|
1003
|
+
if (Array.isArray(schema.entities)) {
|
|
1004
|
+
return schema.entities ?? [];
|
|
1005
|
+
}
|
|
1006
|
+
const tables = schema.tables;
|
|
1007
|
+
if (!Array.isArray(tables)) return [];
|
|
1008
|
+
return tables.map((table) => ({
|
|
1009
|
+
name: table.name,
|
|
1010
|
+
attributes: table.attributes ?? []
|
|
1011
|
+
}));
|
|
1012
|
+
}
|
|
1013
|
+
function normalizePartition(partition) {
|
|
1014
|
+
if (partition == null) return "";
|
|
1015
|
+
const trimmed = partition.trim();
|
|
1016
|
+
return trimmed;
|
|
1017
|
+
}
|
|
1018
|
+
function identifiersEqual(a, b) {
|
|
1019
|
+
if (!a && !b) return true;
|
|
1020
|
+
if (!a || !b) return false;
|
|
1021
|
+
return a.name === b.name && a.generator === b.generator && a.type === b.type;
|
|
1022
|
+
}
|
|
1023
|
+
function diffAttributes(apiAttrs, localAttrs) {
|
|
1024
|
+
const apiMap = mapByName(apiAttrs);
|
|
1025
|
+
const localMap = mapByName(localAttrs);
|
|
1026
|
+
const added = [];
|
|
1027
|
+
const removed = [];
|
|
1028
|
+
const changed = [];
|
|
1029
|
+
for (const [name, local] of localMap.entries()) {
|
|
1030
|
+
if (!apiMap.has(name)) {
|
|
1031
|
+
added.push(local);
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const api = apiMap.get(name);
|
|
1035
|
+
const apiNull = Boolean(api.isNullable);
|
|
1036
|
+
const localNull = Boolean(local.isNullable);
|
|
1037
|
+
if (api.type !== local.type || apiNull !== localNull) {
|
|
1038
|
+
changed.push({
|
|
1039
|
+
name,
|
|
1040
|
+
from: { type: api.type, isNullable: apiNull },
|
|
1041
|
+
to: { type: local.type, isNullable: localNull }
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
for (const name of apiMap.keys()) {
|
|
1046
|
+
if (!localMap.has(name)) removed.push(name);
|
|
1047
|
+
}
|
|
1048
|
+
added.sort((a, b) => a.name.localeCompare(b.name));
|
|
1049
|
+
removed.sort();
|
|
1050
|
+
changed.sort((a, b) => a.name.localeCompare(b.name));
|
|
1051
|
+
if (!added.length && !removed.length && !changed.length) return null;
|
|
1052
|
+
return { added, removed, changed };
|
|
1053
|
+
}
|
|
1054
|
+
function diffIndexes(apiIndexes, localIndexes) {
|
|
1055
|
+
const apiMap = mapByName(apiIndexes);
|
|
1056
|
+
const localMap = mapByName(localIndexes);
|
|
1057
|
+
const added = [];
|
|
1058
|
+
const removed = [];
|
|
1059
|
+
const changed = [];
|
|
1060
|
+
for (const [name, local] of localMap.entries()) {
|
|
1061
|
+
if (!apiMap.has(name)) {
|
|
1062
|
+
added.push(local);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const api = apiMap.get(name);
|
|
1066
|
+
const apiType = api.type ?? "DEFAULT";
|
|
1067
|
+
const localType = local.type ?? "DEFAULT";
|
|
1068
|
+
const apiScore = api.minimumScore;
|
|
1069
|
+
const localScore = local.minimumScore;
|
|
1070
|
+
if (apiType !== localType || apiScore !== localScore) {
|
|
1071
|
+
changed.push({ name, from: api, to: local });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const name of apiMap.keys()) {
|
|
1075
|
+
if (!localMap.has(name)) removed.push(name);
|
|
1076
|
+
}
|
|
1077
|
+
added.sort((a, b) => a.name.localeCompare(b.name));
|
|
1078
|
+
removed.sort();
|
|
1079
|
+
changed.sort((a, b) => a.name.localeCompare(b.name));
|
|
1080
|
+
if (!added.length && !removed.length && !changed.length) return null;
|
|
1081
|
+
return { added, removed, changed };
|
|
1082
|
+
}
|
|
1083
|
+
function diffResolvers(apiResolvers, localResolvers) {
|
|
1084
|
+
const apiMap = mapByName(apiResolvers);
|
|
1085
|
+
const localMap = mapByName(localResolvers);
|
|
1086
|
+
const added = [];
|
|
1087
|
+
const removed = [];
|
|
1088
|
+
const changed = [];
|
|
1089
|
+
for (const [name, local] of localMap.entries()) {
|
|
1090
|
+
if (!apiMap.has(name)) {
|
|
1091
|
+
added.push(local);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const api = apiMap.get(name);
|
|
1095
|
+
if (api.resolver !== local.resolver) {
|
|
1096
|
+
changed.push({ name, from: api, to: local });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
for (const name of apiMap.keys()) {
|
|
1100
|
+
if (!localMap.has(name)) removed.push(name);
|
|
1101
|
+
}
|
|
1102
|
+
added.sort((a, b) => a.name.localeCompare(b.name));
|
|
1103
|
+
removed.sort();
|
|
1104
|
+
changed.sort((a, b) => a.name.localeCompare(b.name));
|
|
1105
|
+
if (!added.length && !removed.length && !changed.length) return null;
|
|
1106
|
+
return { added, removed, changed };
|
|
1107
|
+
}
|
|
1108
|
+
function diffTriggers(apiTriggers, localTriggers) {
|
|
1109
|
+
const apiMap = mapByName(apiTriggers);
|
|
1110
|
+
const localMap = mapByName(localTriggers);
|
|
1111
|
+
const added = [];
|
|
1112
|
+
const removed = [];
|
|
1113
|
+
const changed = [];
|
|
1114
|
+
for (const [name, local] of localMap.entries()) {
|
|
1115
|
+
if (!apiMap.has(name)) {
|
|
1116
|
+
added.push(local);
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const api = apiMap.get(name);
|
|
1120
|
+
if (api.event !== local.event || api.trigger !== local.trigger) {
|
|
1121
|
+
changed.push({ name, from: api, to: local });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
for (const name of apiMap.keys()) {
|
|
1125
|
+
if (!localMap.has(name)) removed.push(name);
|
|
1126
|
+
}
|
|
1127
|
+
added.sort((a, b) => a.name.localeCompare(b.name));
|
|
1128
|
+
removed.sort();
|
|
1129
|
+
changed.sort((a, b) => a.name.localeCompare(b.name));
|
|
1130
|
+
if (!added.length && !removed.length && !changed.length) return null;
|
|
1131
|
+
return { added, removed, changed };
|
|
1132
|
+
}
|
|
1133
|
+
function computeSchemaDiff(apiSchema, localSchema) {
|
|
1134
|
+
const apiEntities = normalizeEntities(apiSchema);
|
|
1135
|
+
const localEntities = normalizeEntities(localSchema);
|
|
1136
|
+
const apiMap = mapByName(apiEntities);
|
|
1137
|
+
const localMap = mapByName(localEntities);
|
|
1138
|
+
const newTables = [];
|
|
1139
|
+
const removedTables = [];
|
|
1140
|
+
const changedTables = [];
|
|
1141
|
+
for (const [name, localEntity] of localMap.entries()) {
|
|
1142
|
+
if (!apiMap.has(name)) {
|
|
1143
|
+
newTables.push(name);
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
const apiEntity = apiMap.get(name);
|
|
1147
|
+
const tableDiff = { name };
|
|
1148
|
+
const partitionFrom = normalizePartition(apiEntity.partition);
|
|
1149
|
+
const partitionTo = normalizePartition(localEntity.partition);
|
|
1150
|
+
if (partitionFrom !== partitionTo) {
|
|
1151
|
+
tableDiff.partition = { from: partitionFrom || null, to: partitionTo || null };
|
|
1152
|
+
}
|
|
1153
|
+
if (!identifiersEqual(apiEntity.identifier, localEntity.identifier)) {
|
|
1154
|
+
tableDiff.identifier = {
|
|
1155
|
+
from: apiEntity.identifier ?? null,
|
|
1156
|
+
to: localEntity.identifier ?? null
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
const attrs = diffAttributes(apiEntity.attributes, localEntity.attributes);
|
|
1160
|
+
if (attrs) tableDiff.attributes = attrs;
|
|
1161
|
+
const indexes = diffIndexes(apiEntity.indexes, localEntity.indexes);
|
|
1162
|
+
if (indexes) tableDiff.indexes = indexes;
|
|
1163
|
+
const resolvers = diffResolvers(apiEntity.resolvers, localEntity.resolvers);
|
|
1164
|
+
if (resolvers) tableDiff.resolvers = resolvers;
|
|
1165
|
+
const triggers = diffTriggers(apiEntity.triggers, localEntity.triggers);
|
|
1166
|
+
if (triggers) tableDiff.triggers = triggers;
|
|
1167
|
+
const hasChange = tableDiff.partition || tableDiff.identifier || tableDiff.attributes || tableDiff.indexes || tableDiff.resolvers || tableDiff.triggers;
|
|
1168
|
+
if (hasChange) {
|
|
1169
|
+
changedTables.push(tableDiff);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
for (const name of apiMap.keys()) {
|
|
1173
|
+
if (!localMap.has(name)) removedTables.push(name);
|
|
1174
|
+
}
|
|
1175
|
+
newTables.sort();
|
|
1176
|
+
removedTables.sort();
|
|
1177
|
+
changedTables.sort((a, b) => a.name.localeCompare(b.name));
|
|
1178
|
+
return { newTables, removedTables, changedTables };
|
|
1179
|
+
}
|
|
1180
|
+
function isScalar(value) {
|
|
1181
|
+
return value == null || value instanceof Date || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
1182
|
+
}
|
|
1183
|
+
function formatScalar(value) {
|
|
1184
|
+
if (value === null || value === void 0) return "null";
|
|
1185
|
+
if (value instanceof Date) return JSON.stringify(value.toISOString());
|
|
1186
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1187
|
+
return String(value);
|
|
1188
|
+
}
|
|
1189
|
+
function isRecord(value) {
|
|
1190
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
|
|
1191
|
+
}
|
|
1192
|
+
function toYamlLines(value, indent = 0) {
|
|
1193
|
+
const pad = " ".repeat(indent);
|
|
1194
|
+
if (Array.isArray(value)) {
|
|
1195
|
+
if (!value.length) return [`${pad}[]`];
|
|
1196
|
+
const lines = [];
|
|
1197
|
+
for (const item of value) {
|
|
1198
|
+
if (isScalar(item)) {
|
|
1199
|
+
lines.push(`${pad}- ${formatScalar(item)}`);
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
1202
|
+
const nested = toYamlLines(item, indent + 2);
|
|
1203
|
+
const [first, ...rest] = nested;
|
|
1204
|
+
lines.push(`${pad}- ${first ? first.trimStart() : "{}"}`);
|
|
1205
|
+
for (const line of rest) lines.push(line);
|
|
1206
|
+
}
|
|
1207
|
+
return lines;
|
|
1208
|
+
}
|
|
1209
|
+
if (isRecord(value)) {
|
|
1210
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0);
|
|
1211
|
+
if (!entries.length) return [`${pad}{}`];
|
|
1212
|
+
const lines = [];
|
|
1213
|
+
for (const [key, val] of entries) {
|
|
1214
|
+
if (Array.isArray(val) && val.length === 0) {
|
|
1215
|
+
lines.push(`${pad}${key}: []`);
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (isScalar(val)) {
|
|
1219
|
+
lines.push(`${pad}${key}: ${formatScalar(val)}`);
|
|
1220
|
+
} else {
|
|
1221
|
+
lines.push(`${pad}${key}:`);
|
|
1222
|
+
lines.push(...toYamlLines(val, indent + 2));
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return lines;
|
|
1226
|
+
}
|
|
1227
|
+
return [`${pad}${formatScalar(value)}`];
|
|
1228
|
+
}
|
|
1229
|
+
function pruneTableDiff(table) {
|
|
1230
|
+
const pruned = { name: table.name };
|
|
1231
|
+
if (table.partition && table.partition.from !== table.partition.to) {
|
|
1232
|
+
pruned.partition = table.partition;
|
|
1233
|
+
}
|
|
1234
|
+
if (table.identifier && !identifiersEqual(table.identifier.from ?? void 0, table.identifier.to ?? void 0)) {
|
|
1235
|
+
pruned.identifier = table.identifier;
|
|
1236
|
+
}
|
|
1237
|
+
if (table.attributes) {
|
|
1238
|
+
const { added = [], removed = [], changed = [] } = table.attributes;
|
|
1239
|
+
if (added.length || removed.length || changed.length) {
|
|
1240
|
+
pruned.attributes = { added, removed, changed };
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (table.indexes) {
|
|
1244
|
+
const { added = [], removed = [], changed = [] } = table.indexes;
|
|
1245
|
+
if (added.length || removed.length || changed.length) {
|
|
1246
|
+
pruned.indexes = { added, removed, changed };
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (table.resolvers) {
|
|
1250
|
+
const { added = [], removed = [], changed = [] } = table.resolvers;
|
|
1251
|
+
if (added.length || removed.length || changed.length) {
|
|
1252
|
+
pruned.resolvers = { added, removed, changed };
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (table.triggers) {
|
|
1256
|
+
const { added = [], removed = [], changed = [] } = table.triggers;
|
|
1257
|
+
if (added.length || removed.length || changed.length) {
|
|
1258
|
+
pruned.triggers = { added, removed, changed };
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const hasChange = pruned.partition || pruned.identifier || pruned.attributes && (pruned.attributes.added.length || pruned.attributes.removed.length || pruned.attributes.changed.length) || pruned.indexes && (pruned.indexes.added.length || pruned.indexes.removed.length || pruned.indexes.changed.length) || pruned.resolvers && (pruned.resolvers.added.length || pruned.resolvers.removed.length || pruned.resolvers.changed.length) || pruned.triggers && (pruned.triggers.added.length || pruned.triggers.removed.length || pruned.triggers.changed.length);
|
|
1262
|
+
return hasChange ? pruned : null;
|
|
1263
|
+
}
|
|
1264
|
+
function formatSchemaDiff(diff, filePath) {
|
|
1265
|
+
const hasChanges = diff.newTables.length || diff.removedTables.length || diff.changedTables.length;
|
|
1266
|
+
if (!hasChanges) {
|
|
1267
|
+
return `No differences found between API schema and ${filePath ?? "local schema"}.
|
|
1268
|
+
`;
|
|
1269
|
+
}
|
|
1270
|
+
const header = filePath ? `# Diff between API schema and ${filePath}
|
|
1271
|
+
` : "# Schema diff";
|
|
1272
|
+
const prunedTables = diff.changedTables.map(pruneTableDiff).filter((t) => Boolean(t));
|
|
1273
|
+
const yamlObject = {
|
|
1274
|
+
newTables: [...diff.newTables],
|
|
1275
|
+
removedTables: [...diff.removedTables],
|
|
1276
|
+
changedTables: prunedTables
|
|
1277
|
+
};
|
|
1278
|
+
const bodyLines = toYamlLines(yamlObject);
|
|
1279
|
+
return `${header}
|
|
1280
|
+
${bodyLines.join("\n")}
|
|
1281
|
+
`;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
919
1284
|
// src/impl/onyx.ts
|
|
920
1285
|
var DEFAULT_CACHE_TTL = 5 * 60 * 1e3;
|
|
921
1286
|
var cachedCfg = null;
|
|
@@ -969,6 +1334,10 @@ function serializeDates(value) {
|
|
|
969
1334
|
}
|
|
970
1335
|
return value;
|
|
971
1336
|
}
|
|
1337
|
+
function stripEntityText(input) {
|
|
1338
|
+
const { entityText, ...rest } = input;
|
|
1339
|
+
return rest;
|
|
1340
|
+
}
|
|
972
1341
|
function normalizeSecretMetadata(input) {
|
|
973
1342
|
return { ...input, updatedAt: new Date(input.updatedAt) };
|
|
974
1343
|
}
|
|
@@ -982,7 +1351,20 @@ function normalizeDate(value) {
|
|
|
982
1351
|
return Number.isNaN(ts.getTime()) ? void 0 : ts;
|
|
983
1352
|
}
|
|
984
1353
|
function normalizeSchemaRevision(input, fallbackDatabaseId) {
|
|
985
|
-
const {
|
|
1354
|
+
const {
|
|
1355
|
+
meta,
|
|
1356
|
+
createdAt,
|
|
1357
|
+
publishedAt,
|
|
1358
|
+
revisionId,
|
|
1359
|
+
entityText,
|
|
1360
|
+
databaseId,
|
|
1361
|
+
entities,
|
|
1362
|
+
revisionDescription,
|
|
1363
|
+
...rest
|
|
1364
|
+
} = input;
|
|
1365
|
+
const dbId = typeof databaseId === "string" ? databaseId : fallbackDatabaseId;
|
|
1366
|
+
const entityList = Array.isArray(entities) ? entities : [];
|
|
1367
|
+
const revisionDesc = typeof revisionDescription === "string" ? revisionDescription : void 0;
|
|
986
1368
|
const mergedMeta = {
|
|
987
1369
|
revisionId: meta?.revisionId ?? revisionId,
|
|
988
1370
|
createdAt: normalizeDate(meta?.createdAt ?? createdAt),
|
|
@@ -990,10 +1372,11 @@ function normalizeSchemaRevision(input, fallbackDatabaseId) {
|
|
|
990
1372
|
};
|
|
991
1373
|
const cleanedMeta = mergedMeta.revisionId || mergedMeta.createdAt || mergedMeta.publishedAt ? mergedMeta : void 0;
|
|
992
1374
|
return {
|
|
993
|
-
|
|
994
|
-
|
|
1375
|
+
databaseId: dbId,
|
|
1376
|
+
revisionDescription: revisionDesc,
|
|
1377
|
+
entities: entityList,
|
|
995
1378
|
meta: cleanedMeta,
|
|
996
|
-
|
|
1379
|
+
...rest
|
|
997
1380
|
};
|
|
998
1381
|
}
|
|
999
1382
|
var OnyxDatabaseImpl = class {
|
|
@@ -1105,7 +1488,8 @@ var OnyxDatabaseImpl = class {
|
|
|
1105
1488
|
const path2 = `/data/${encodeURIComponent(databaseId)}/${encodeURIComponent(
|
|
1106
1489
|
table
|
|
1107
1490
|
)}/${encodeURIComponent(primaryKey)}${params.toString() ? `?${params.toString()}` : ""}`;
|
|
1108
|
-
|
|
1491
|
+
await http.request("DELETE", path2);
|
|
1492
|
+
return true;
|
|
1109
1493
|
}
|
|
1110
1494
|
async saveDocument(doc) {
|
|
1111
1495
|
const { http, databaseId } = await this.ensureClient();
|
|
@@ -1148,20 +1532,32 @@ var OnyxDatabaseImpl = class {
|
|
|
1148
1532
|
const res = await http.request("GET", path2);
|
|
1149
1533
|
return Array.isArray(res) ? res.map((entry) => normalizeSchemaRevision(entry, databaseId)) : [];
|
|
1150
1534
|
}
|
|
1535
|
+
async diffSchema(localSchema) {
|
|
1536
|
+
const apiSchema = await this.getSchema();
|
|
1537
|
+
return computeSchemaDiff(apiSchema, localSchema);
|
|
1538
|
+
}
|
|
1151
1539
|
async updateSchema(schema, options) {
|
|
1152
1540
|
const { http, databaseId } = await this.ensureClient();
|
|
1153
1541
|
const params = new URLSearchParams();
|
|
1154
1542
|
if (options?.publish) params.append("publish", "true");
|
|
1155
1543
|
const path2 = `/schemas/${encodeURIComponent(databaseId)}${params.size ? `?${params.toString()}` : ""}`;
|
|
1156
|
-
const body = { ...schema, databaseId: schema.databaseId ?? databaseId };
|
|
1157
|
-
const res = await http.request(
|
|
1544
|
+
const body = stripEntityText({ ...schema, databaseId: schema.databaseId ?? databaseId });
|
|
1545
|
+
const res = await http.request(
|
|
1546
|
+
"PUT",
|
|
1547
|
+
path2,
|
|
1548
|
+
serializeDates(body)
|
|
1549
|
+
);
|
|
1158
1550
|
return normalizeSchemaRevision(res, databaseId);
|
|
1159
1551
|
}
|
|
1160
1552
|
async validateSchema(schema) {
|
|
1161
1553
|
const { http, databaseId } = await this.ensureClient();
|
|
1162
1554
|
const path2 = `/schemas/${encodeURIComponent(databaseId)}/validate`;
|
|
1163
|
-
const body = { ...schema, databaseId: schema.databaseId ?? databaseId };
|
|
1164
|
-
const res = await http.request(
|
|
1555
|
+
const body = stripEntityText({ ...schema, databaseId: schema.databaseId ?? databaseId });
|
|
1556
|
+
const res = await http.request(
|
|
1557
|
+
"POST",
|
|
1558
|
+
path2,
|
|
1559
|
+
serializeDates(body)
|
|
1560
|
+
);
|
|
1165
1561
|
const normalizedSchema = res.schema ? normalizeSchemaRevision(res.schema, databaseId) : void 0;
|
|
1166
1562
|
return {
|
|
1167
1563
|
...res,
|
|
@@ -1323,11 +1719,14 @@ var QueryBuilderImpl = class {
|
|
|
1323
1719
|
if (!this.table) throw new Error("Table is not defined. Call from(<table>) first.");
|
|
1324
1720
|
return this.table;
|
|
1325
1721
|
}
|
|
1722
|
+
serializableConditions() {
|
|
1723
|
+
return normalizeCondition(this.conditions);
|
|
1724
|
+
}
|
|
1326
1725
|
toSelectQuery() {
|
|
1327
1726
|
return {
|
|
1328
1727
|
type: "SelectQuery",
|
|
1329
1728
|
fields: this.fields,
|
|
1330
|
-
conditions: this.
|
|
1729
|
+
conditions: this.serializableConditions(),
|
|
1331
1730
|
sort: this.sort,
|
|
1332
1731
|
limit: this.limitValue,
|
|
1333
1732
|
distinct: this.distinctValue,
|
|
@@ -1336,6 +1735,21 @@ var QueryBuilderImpl = class {
|
|
|
1336
1735
|
resolvers: this.resolvers
|
|
1337
1736
|
};
|
|
1338
1737
|
}
|
|
1738
|
+
toUpdateQuery() {
|
|
1739
|
+
return {
|
|
1740
|
+
type: "UpdateQuery",
|
|
1741
|
+
conditions: this.serializableConditions(),
|
|
1742
|
+
updates: this.updates ?? {},
|
|
1743
|
+
sort: this.sort,
|
|
1744
|
+
limit: this.limitValue,
|
|
1745
|
+
partition: this.partitionValue ?? null
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
toSerializableQueryObject() {
|
|
1749
|
+
const table = this.ensureTable();
|
|
1750
|
+
const payload = this.mode === "update" ? this.toUpdateQuery() : this.toSelectQuery();
|
|
1751
|
+
return { ...payload, table };
|
|
1752
|
+
}
|
|
1339
1753
|
from(table) {
|
|
1340
1754
|
this.table = table;
|
|
1341
1755
|
return this;
|
|
@@ -1471,14 +1885,7 @@ var QueryBuilderImpl = class {
|
|
|
1471
1885
|
async update() {
|
|
1472
1886
|
if (this.mode !== "update") throw new Error("Call setUpdates(...) before update().");
|
|
1473
1887
|
const table = this.ensureTable();
|
|
1474
|
-
const update =
|
|
1475
|
-
type: "UpdateQuery",
|
|
1476
|
-
conditions: this.conditions,
|
|
1477
|
-
updates: this.updates ?? {},
|
|
1478
|
-
sort: this.sort,
|
|
1479
|
-
limit: this.limitValue,
|
|
1480
|
-
partition: this.partitionValue ?? null
|
|
1481
|
-
};
|
|
1888
|
+
const update = this.toUpdateQuery();
|
|
1482
1889
|
return this.db._update(table, update, this.partitionValue);
|
|
1483
1890
|
}
|
|
1484
1891
|
onItemAdded(listener) {
|
|
@@ -1570,10 +1977,13 @@ Usage:
|
|
|
1570
1977
|
onyx-schema publish [file]
|
|
1571
1978
|
onyx-schema get [file] [--tables tableA,tableB]
|
|
1572
1979
|
onyx-schema validate [file]
|
|
1980
|
+
onyx-schema diff [file]
|
|
1981
|
+
onyx-schema info
|
|
1573
1982
|
|
|
1574
1983
|
Options:
|
|
1575
1984
|
[file] Path to schema JSON (default: ./onyx.schema.json)
|
|
1576
1985
|
--tables <list> Comma-separated list of tables to fetch (for get)
|
|
1986
|
+
--print Print the fetched schema to stdout instead of writing a file
|
|
1577
1987
|
-h, --help Show this help message
|
|
1578
1988
|
`);
|
|
1579
1989
|
}
|
|
@@ -1585,7 +1995,7 @@ function parseTables(value) {
|
|
|
1585
1995
|
function parseArgs(argv) {
|
|
1586
1996
|
const cmd = (argv[2] ?? "").toLowerCase();
|
|
1587
1997
|
let command = "help";
|
|
1588
|
-
if (cmd === "publish" || cmd === "get" || cmd === "validate") {
|
|
1998
|
+
if (cmd === "publish" || cmd === "get" || cmd === "validate" || cmd === "diff" || cmd === "info") {
|
|
1589
1999
|
command = cmd;
|
|
1590
2000
|
}
|
|
1591
2001
|
let idx = 3;
|
|
@@ -1595,6 +2005,7 @@ function parseArgs(argv) {
|
|
|
1595
2005
|
idx++;
|
|
1596
2006
|
}
|
|
1597
2007
|
let tables;
|
|
2008
|
+
let print = false;
|
|
1598
2009
|
for (; idx < argv.length; idx++) {
|
|
1599
2010
|
const arg = argv[idx];
|
|
1600
2011
|
switch (arg) {
|
|
@@ -1602,6 +2013,9 @@ function parseArgs(argv) {
|
|
|
1602
2013
|
tables = parseTables(argv[idx + 1]);
|
|
1603
2014
|
idx++;
|
|
1604
2015
|
break;
|
|
2016
|
+
case "--print":
|
|
2017
|
+
print = true;
|
|
2018
|
+
break;
|
|
1605
2019
|
case "-h":
|
|
1606
2020
|
case "--help":
|
|
1607
2021
|
command = "help";
|
|
@@ -1616,7 +2030,7 @@ function parseArgs(argv) {
|
|
|
1616
2030
|
}
|
|
1617
2031
|
}
|
|
1618
2032
|
}
|
|
1619
|
-
return { command, filePath, tables };
|
|
2033
|
+
return { command, filePath, tables, print };
|
|
1620
2034
|
}
|
|
1621
2035
|
async function readFileJson(filePath) {
|
|
1622
2036
|
const fs = await import('fs/promises');
|
|
@@ -1631,10 +2045,10 @@ async function writeFileJson(filePath, data) {
|
|
|
1631
2045
|
`;
|
|
1632
2046
|
await fs.writeFile(resolved, serialized, "utf8");
|
|
1633
2047
|
}
|
|
1634
|
-
async function fetchSchema(filePath, tables) {
|
|
2048
|
+
async function fetchSchema(filePath, tables, print) {
|
|
1635
2049
|
const db = onyx.init();
|
|
1636
2050
|
const schema = await db.getSchema({ tables });
|
|
1637
|
-
if (tables?.length) {
|
|
2051
|
+
if (tables?.length || print) {
|
|
1638
2052
|
process__default.default.stdout.write(`${JSON.stringify(schema, null, 2)}
|
|
1639
2053
|
`);
|
|
1640
2054
|
return;
|
|
@@ -1670,6 +2084,47 @@ ${formatSchemaErrors(result.errors)}`);
|
|
|
1670
2084
|
process__default.default.stdout.write(`Schema published for database ${revision.databaseId} from ${filePath}.
|
|
1671
2085
|
`);
|
|
1672
2086
|
}
|
|
2087
|
+
async function diffSchema(filePath) {
|
|
2088
|
+
const db = onyx.init();
|
|
2089
|
+
const localSchema = await readFileJson(filePath);
|
|
2090
|
+
const diff = await db.diffSchema(localSchema);
|
|
2091
|
+
const output = formatSchemaDiff(diff, filePath);
|
|
2092
|
+
process__default.default.stdout.write(output);
|
|
2093
|
+
}
|
|
2094
|
+
function maskValue(value) {
|
|
2095
|
+
if (!value) return "";
|
|
2096
|
+
if (value.length <= 4) return value;
|
|
2097
|
+
return `${value.slice(0, 2)}...${value.slice(-2)}`;
|
|
2098
|
+
}
|
|
2099
|
+
function formatSource(source) {
|
|
2100
|
+
if (!source) return "unknown";
|
|
2101
|
+
if (source.includes("env")) return "env";
|
|
2102
|
+
if (source.includes("file") || source.includes("profile") || source.includes("config")) return "file";
|
|
2103
|
+
if (source === "explicit config") return "explicit";
|
|
2104
|
+
return source;
|
|
2105
|
+
}
|
|
2106
|
+
async function printInfo() {
|
|
2107
|
+
const resolved = await resolveConfigWithSource();
|
|
2108
|
+
const db = onyx.init();
|
|
2109
|
+
const cfgFile = resolved.sources.configPath ?? resolved.sources.projectFile ?? resolved.sources.homeProfile ?? "(none)";
|
|
2110
|
+
let connection = "ok";
|
|
2111
|
+
try {
|
|
2112
|
+
await db.getSchema({ tables: [] });
|
|
2113
|
+
} catch (err) {
|
|
2114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2115
|
+
connection = msg;
|
|
2116
|
+
}
|
|
2117
|
+
const lines = [
|
|
2118
|
+
`Database ID: ${resolved.databaseId} (source: ${formatSource(resolved.sources.databaseId)})`,
|
|
2119
|
+
`Base URL : ${resolved.baseUrl} (source: ${formatSource(resolved.sources.baseUrl)})`,
|
|
2120
|
+
`API Key : ${maskValue(resolved.apiKey)} (source: ${formatSource(resolved.sources.apiKey)})`,
|
|
2121
|
+
`API Secret : ${maskValue(resolved.apiSecret)} (source: ${formatSource(resolved.sources.apiSecret)})`,
|
|
2122
|
+
`Config file: ${cfgFile}`,
|
|
2123
|
+
`Connection : ${connection}`
|
|
2124
|
+
];
|
|
2125
|
+
process__default.default.stdout.write(`${lines.join("\n")}
|
|
2126
|
+
`);
|
|
2127
|
+
}
|
|
1673
2128
|
(async () => {
|
|
1674
2129
|
try {
|
|
1675
2130
|
const parsed = parseArgs(process__default.default.argv);
|
|
@@ -1678,11 +2133,17 @@ ${formatSchemaErrors(result.errors)}`);
|
|
|
1678
2133
|
await publishSchema(parsed.filePath);
|
|
1679
2134
|
break;
|
|
1680
2135
|
case "get":
|
|
1681
|
-
await fetchSchema(parsed.filePath, parsed.tables);
|
|
2136
|
+
await fetchSchema(parsed.filePath, parsed.tables, parsed.print);
|
|
1682
2137
|
break;
|
|
1683
2138
|
case "validate":
|
|
1684
2139
|
await validateSchema(parsed.filePath);
|
|
1685
2140
|
break;
|
|
2141
|
+
case "diff":
|
|
2142
|
+
await diffSchema(parsed.filePath);
|
|
2143
|
+
break;
|
|
2144
|
+
case "info":
|
|
2145
|
+
await printInfo();
|
|
2146
|
+
break;
|
|
1686
2147
|
default:
|
|
1687
2148
|
printHelp();
|
|
1688
2149
|
return;
|