@pvidaal07/heydb 0.0.0-stage → 2.0.0-beta.0
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/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +149 -2
- package/extensions/commands.ts +732 -0
- package/extensions/context.ts +37 -0
- package/extensions/deps.ts +32 -0
- package/extensions/heydb.ts +20 -0
- package/extensions/secret-input.ts +120 -0
- package/extensions/tools.ts +402 -0
- package/lib/about.ts +7 -0
- package/lib/config/operations.ts +75 -0
- package/lib/config/store.ts +73 -0
- package/lib/config/types.ts +37 -0
- package/lib/config/validate.ts +136 -0
- package/lib/engines/index.ts +66 -0
- package/lib/engines/mysql/catalog.ts +371 -0
- package/lib/engines/mysql/introspect.ts +186 -0
- package/lib/engines/postgres/catalog.ts +390 -0
- package/lib/engines/postgres/introspect.ts +240 -0
- package/lib/knowledge/operations.ts +223 -0
- package/lib/knowledge/query.ts +129 -0
- package/lib/knowledge/types.ts +64 -0
- package/lib/knowledge/validate.ts +151 -0
- package/lib/model/diff.ts +218 -0
- package/lib/model/hash.ts +88 -0
- package/lib/model/names.ts +141 -0
- package/lib/model/order.ts +47 -0
- package/lib/model/references.ts +251 -0
- package/lib/model/snapshot.ts +128 -0
- package/lib/model/stats.ts +97 -0
- package/lib/model/validate.ts +240 -0
- package/lib/render/markdown.ts +508 -0
- package/lib/search.ts +530 -0
- package/lib/secrets.ts +149 -0
- package/lib/store/envfile.ts +124 -0
- package/lib/store/files.ts +57 -0
- package/lib/store/guards.ts +68 -0
- package/lib/store/knowledge.ts +139 -0
- package/lib/store/project.ts +28 -0
- package/lib/store/snapshots.ts +142 -0
- package/lib/usecases/author.ts +51 -0
- package/lib/usecases/code-scan.ts +435 -0
- package/lib/usecases/init.ts +111 -0
- package/lib/usecases/knowledge.ts +214 -0
- package/lib/usecases/project.ts +377 -0
- package/lib/usecases/resolve.ts +186 -0
- package/lib/usecases/sharing.ts +83 -0
- package/lib/usecases/status.ts +37 -0
- package/lib/usecases/sync.ts +33 -0
- package/package.json +69 -5
- package/skills/heydb/SKILL.md +67 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Column,
|
|
3
|
+
Database,
|
|
4
|
+
EngineKind,
|
|
5
|
+
ForeignKey,
|
|
6
|
+
GeneratedColumn,
|
|
7
|
+
Index,
|
|
8
|
+
ReferentialAction,
|
|
9
|
+
Routine,
|
|
10
|
+
Schema,
|
|
11
|
+
Snapshot,
|
|
12
|
+
Table,
|
|
13
|
+
TableKind,
|
|
14
|
+
Trigger,
|
|
15
|
+
Warning,
|
|
16
|
+
} from "./snapshot.ts";
|
|
17
|
+
|
|
18
|
+
// Snapshot files come from disk, and .heydb/ is committed: a teammate's old
|
|
19
|
+
// heydb, a bad merge or a hand edit can put anything in them. TypeScript types
|
|
20
|
+
// vanish at runtime, so this checks every field of every object once, at the
|
|
21
|
+
// boundary, and the rest of heydb can trust the Snapshot type.
|
|
22
|
+
//
|
|
23
|
+
// The result is built field by field from the input, so unknown fields are
|
|
24
|
+
// dropped and never written back. Errors name the exact path of the first bad
|
|
25
|
+
// value, e.g. "databases[0].schemas[0].tables[2].columns[1].nullable must be a boolean".
|
|
26
|
+
|
|
27
|
+
const ENGINE_KINDS: readonly EngineKind[] = ["mysql", "mariadb", "postgresql"];
|
|
28
|
+
const TABLE_KINDS: readonly TableKind[] = ["table", "view"];
|
|
29
|
+
const ROUTINE_KINDS: readonly Routine["kind"][] = ["procedure", "function"];
|
|
30
|
+
const REFERENTIAL_ACTIONS: readonly ReferentialAction[] = ["CASCADE", "SET NULL", "SET DEFAULT", "RESTRICT", "NO ACTION"];
|
|
31
|
+
const TRIGGER_TIMINGS: readonly Trigger["timing"][] = ["BEFORE", "AFTER", "INSTEAD OF"];
|
|
32
|
+
const TRIGGER_EVENTS: readonly Trigger["events"][number][] = ["INSERT", "UPDATE", "DELETE", "TRUNCATE"];
|
|
33
|
+
|
|
34
|
+
export function validateSnapshot(data: unknown): Snapshot {
|
|
35
|
+
const root = object(data, "snapshot");
|
|
36
|
+
const at = reader(root, "");
|
|
37
|
+
if (at.value("formatVersion") !== 1) throw new Error("formatVersion must be 1");
|
|
38
|
+
const engine = at.object("engine");
|
|
39
|
+
return {
|
|
40
|
+
formatVersion: 1,
|
|
41
|
+
engine: {
|
|
42
|
+
kind: engine.oneOf("kind", ENGINE_KINDS),
|
|
43
|
+
version: engine.string("version"),
|
|
44
|
+
},
|
|
45
|
+
capturedAt: at.string("capturedAt"),
|
|
46
|
+
databases: at.list("databases", validateDatabase),
|
|
47
|
+
warnings: at.list("warnings", validateWarning),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateDatabase(at: Reader): Database {
|
|
52
|
+
return { name: at.string("name"), schemas: at.list("schemas", validateSchema) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateSchema(at: Reader): Schema {
|
|
56
|
+
return {
|
|
57
|
+
name: at.string("name"),
|
|
58
|
+
tables: at.list("tables", validateTable),
|
|
59
|
+
routines: at.list("routines", (routine) => ({
|
|
60
|
+
name: routine.string("name"),
|
|
61
|
+
kind: routine.oneOf("kind", ROUTINE_KINDS),
|
|
62
|
+
})),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function validateTable(at: Reader): Table {
|
|
67
|
+
return {
|
|
68
|
+
name: at.string("name"),
|
|
69
|
+
kind: at.oneOf("kind", TABLE_KINDS),
|
|
70
|
+
comment: at.string("comment"),
|
|
71
|
+
storageEngine: at.stringOrNull("storageEngine"),
|
|
72
|
+
rowEstimate: at.countOrNull("rowEstimate"),
|
|
73
|
+
columns: at.list("columns", validateColumn),
|
|
74
|
+
primaryKey: at.strings("primaryKey"),
|
|
75
|
+
indexes: at.list("indexes", validateIndex),
|
|
76
|
+
foreignKeys: at.list("foreignKeys", validateForeignKey),
|
|
77
|
+
triggers: at.list("triggers", validateTrigger),
|
|
78
|
+
viewDefinition: at.stringOrNull("viewDefinition"),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateColumn(at: Reader): Column {
|
|
83
|
+
return {
|
|
84
|
+
name: at.string("name"),
|
|
85
|
+
position: at.position("position"),
|
|
86
|
+
type: at.string("type"),
|
|
87
|
+
nullable: at.boolean("nullable"),
|
|
88
|
+
default: at.stringOrNull("default"),
|
|
89
|
+
comment: at.string("comment"),
|
|
90
|
+
autoIncrement: at.boolean("autoIncrement"),
|
|
91
|
+
generated: at.objectOrNull("generated", validateGenerated),
|
|
92
|
+
collation: at.stringOrNull("collation"),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateGenerated(at: Reader): GeneratedColumn {
|
|
97
|
+
return { expression: at.string("expression"), stored: at.boolean("stored") };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateIndex(at: Reader): Index {
|
|
101
|
+
return {
|
|
102
|
+
name: at.string("name"),
|
|
103
|
+
columns: at.strings("columns"),
|
|
104
|
+
unique: at.boolean("unique"),
|
|
105
|
+
method: at.string("method"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateForeignKey(at: Reader): ForeignKey {
|
|
110
|
+
const references = at.object("references");
|
|
111
|
+
return {
|
|
112
|
+
name: at.string("name"),
|
|
113
|
+
columns: at.strings("columns"),
|
|
114
|
+
references: {
|
|
115
|
+
database: references.string("database"),
|
|
116
|
+
schema: references.string("schema"),
|
|
117
|
+
table: references.string("table"),
|
|
118
|
+
columns: references.strings("columns"),
|
|
119
|
+
},
|
|
120
|
+
onDelete: at.oneOf("onDelete", REFERENTIAL_ACTIONS),
|
|
121
|
+
onUpdate: at.oneOf("onUpdate", REFERENTIAL_ACTIONS),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateTrigger(at: Reader): Trigger {
|
|
126
|
+
return {
|
|
127
|
+
name: at.string("name"),
|
|
128
|
+
timing: at.oneOf("timing", TRIGGER_TIMINGS),
|
|
129
|
+
events: at.values("events", (value, path) => oneOf(value, path, TRIGGER_EVENTS)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function validateWarning(at: Reader): Warning {
|
|
134
|
+
const target = at.value("target");
|
|
135
|
+
if (target !== undefined && typeof target !== "string") {
|
|
136
|
+
throw new Error(`${at.path("target")} must be a string when present`);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
code: at.string("code"),
|
|
140
|
+
message: at.string("message"),
|
|
141
|
+
// Absent stays absent: an optional field is never written as undefined.
|
|
142
|
+
...(target !== undefined && { target }),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A Reader reads the fields of one JSON object and knows its path, so every
|
|
147
|
+
// check can name exactly where the bad value is.
|
|
148
|
+
interface Reader {
|
|
149
|
+
path(key: string): string;
|
|
150
|
+
value(key: string): unknown;
|
|
151
|
+
string(key: string): string;
|
|
152
|
+
stringOrNull(key: string): string | null;
|
|
153
|
+
boolean(key: string): boolean;
|
|
154
|
+
position(key: string): number;
|
|
155
|
+
countOrNull(key: string): number | null;
|
|
156
|
+
oneOf<T extends string>(key: string, allowed: readonly T[]): T;
|
|
157
|
+
strings(key: string): string[];
|
|
158
|
+
object(key: string): Reader;
|
|
159
|
+
objectOrNull<T>(key: string, read: (at: Reader) => T): T | null;
|
|
160
|
+
/** A list of objects. */
|
|
161
|
+
list<T>(key: string, read: (at: Reader) => T): T[];
|
|
162
|
+
/** A list of plain values. */
|
|
163
|
+
values<T>(key: string, read: (value: unknown, path: string) => T): T[];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function reader(record: Record<string, unknown>, prefix: string): Reader {
|
|
167
|
+
const path = (key: string) => (prefix === "" ? key : `${prefix}.${key}`);
|
|
168
|
+
// Every typed read goes through here: a missing field gets its own message,
|
|
169
|
+
// because "is missing" points at an old file, "must be" at a damaged one.
|
|
170
|
+
const read = (key: string): unknown => {
|
|
171
|
+
if (!(key in record)) throw new Error(`${path(key)} is missing`);
|
|
172
|
+
return record[key];
|
|
173
|
+
};
|
|
174
|
+
const fail = (key: string, expected: string): never => {
|
|
175
|
+
throw new Error(`${path(key)} must be ${expected}`);
|
|
176
|
+
};
|
|
177
|
+
const values = <T>(key: string, readItem: (value: unknown, path: string) => T): T[] => {
|
|
178
|
+
const value = read(key);
|
|
179
|
+
if (!Array.isArray(value)) return fail(key, "a list");
|
|
180
|
+
return value.map((item, index) => readItem(item, `${path(key)}[${index}]`));
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
path,
|
|
185
|
+
value: (key) => record[key],
|
|
186
|
+
string(key) {
|
|
187
|
+
const value = read(key);
|
|
188
|
+
return typeof value === "string" ? value : fail(key, "a string");
|
|
189
|
+
},
|
|
190
|
+
stringOrNull(key) {
|
|
191
|
+
const value = read(key);
|
|
192
|
+
return value === null || typeof value === "string" ? value : fail(key, "a string or null");
|
|
193
|
+
},
|
|
194
|
+
boolean(key) {
|
|
195
|
+
const value = read(key);
|
|
196
|
+
return typeof value === "boolean" ? value : fail(key, "a boolean");
|
|
197
|
+
},
|
|
198
|
+
position(key) {
|
|
199
|
+
const value = read(key);
|
|
200
|
+
return Number.isInteger(value) && (value as number) > 0 ? (value as number) : fail(key, "a positive integer");
|
|
201
|
+
},
|
|
202
|
+
countOrNull(key) {
|
|
203
|
+
const value = read(key);
|
|
204
|
+
if (value === null) return null;
|
|
205
|
+
return Number.isInteger(value) && (value as number) >= 0
|
|
206
|
+
? (value as number)
|
|
207
|
+
: fail(key, "a non-negative integer or null");
|
|
208
|
+
},
|
|
209
|
+
oneOf: (key, allowed) => oneOf(read(key), path(key), allowed),
|
|
210
|
+
strings(key) {
|
|
211
|
+
return values(key, (value, itemPath) => {
|
|
212
|
+
if (typeof value !== "string") throw new Error(`${itemPath} must be a string`);
|
|
213
|
+
return value;
|
|
214
|
+
});
|
|
215
|
+
},
|
|
216
|
+
object: (key) => reader(object(read(key), path(key)), path(key)),
|
|
217
|
+
objectOrNull(key, readValue) {
|
|
218
|
+
const value = read(key);
|
|
219
|
+
if (value === null) return null;
|
|
220
|
+
if (!isRecord(value)) return fail(key, "an object or null");
|
|
221
|
+
return readValue(reader(value, path(key)));
|
|
222
|
+
},
|
|
223
|
+
list: (key, readItem) => values(key, (item, itemPath) => readItem(reader(object(item, itemPath), itemPath))),
|
|
224
|
+
values,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function object(value: unknown, path: string): Record<string, unknown> {
|
|
229
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object`);
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
234
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function oneOf<T extends string>(value: unknown, path: string, allowed: readonly T[]): T {
|
|
238
|
+
if (typeof value === "string" && (allowed as readonly string[]).includes(value)) return value as T;
|
|
239
|
+
throw new Error(`${path} must be one of ${allowed.map((item) => JSON.stringify(item)).join(", ")}`);
|
|
240
|
+
}
|