@astrojs/db 0.0.0-db-export-bug-20240307130354
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/LICENSE +59 -0
- package/README.md +38 -0
- package/dist/core/cli/commands/execute/index.d.ts +8 -0
- package/dist/core/cli/commands/execute/index.js +32 -0
- package/dist/core/cli/commands/link/index.d.ts +20 -0
- package/dist/core/cli/commands/link/index.js +238 -0
- package/dist/core/cli/commands/login/index.d.ts +8 -0
- package/dist/core/cli/commands/login/index.js +55 -0
- package/dist/core/cli/commands/logout/index.d.ts +1 -0
- package/dist/core/cli/commands/logout/index.js +9 -0
- package/dist/core/cli/commands/push/index.d.ts +8 -0
- package/dist/core/cli/commands/push/index.js +77 -0
- package/dist/core/cli/commands/shell/index.d.ts +8 -0
- package/dist/core/cli/commands/shell/index.js +17 -0
- package/dist/core/cli/commands/verify/index.d.ts +8 -0
- package/dist/core/cli/commands/verify/index.js +46 -0
- package/dist/core/cli/index.d.ts +6 -0
- package/dist/core/cli/index.js +77 -0
- package/dist/core/cli/migration-queries.d.ts +22 -0
- package/dist/core/cli/migration-queries.js +366 -0
- package/dist/core/consts.d.ts +7 -0
- package/dist/core/consts.js +19 -0
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +65 -0
- package/dist/core/integration/error-map.d.ts +6 -0
- package/dist/core/integration/error-map.js +79 -0
- package/dist/core/integration/file-url.d.ts +2 -0
- package/dist/core/integration/file-url.js +81 -0
- package/dist/core/integration/index.d.ts +2 -0
- package/dist/core/integration/index.js +112 -0
- package/dist/core/integration/typegen.d.ts +5 -0
- package/dist/core/integration/typegen.js +31 -0
- package/dist/core/integration/vite-plugin-db.d.ts +29 -0
- package/dist/core/integration/vite-plugin-db.js +111 -0
- package/dist/core/integration/vite-plugin-inject-env-ts.d.ts +11 -0
- package/dist/core/integration/vite-plugin-inject-env-ts.js +53 -0
- package/dist/core/load-file.d.ts +31 -0
- package/dist/core/load-file.js +98 -0
- package/dist/core/tokens.d.ts +11 -0
- package/dist/core/tokens.js +131 -0
- package/dist/core/types.d.ts +3901 -0
- package/dist/core/types.js +143 -0
- package/dist/core/utils.d.ts +6 -0
- package/dist/core/utils.js +22 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/runtime/config.d.ts +148 -0
- package/dist/runtime/config.js +87 -0
- package/dist/runtime/db-client.d.ts +5 -0
- package/dist/runtime/db-client.js +64 -0
- package/dist/runtime/drizzle.d.ts +1 -0
- package/dist/runtime/drizzle.js +48 -0
- package/dist/runtime/index.d.ts +31 -0
- package/dist/runtime/index.js +126 -0
- package/dist/runtime/queries.d.ts +81 -0
- package/dist/runtime/queries.js +206 -0
- package/dist/runtime/types.d.ts +69 -0
- package/dist/runtime/types.js +8 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +4 -0
- package/index.d.ts +3 -0
- package/package.json +90 -0
- package/virtual.d.ts +9 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { loadDbConfigFile } from "../load-file.js";
|
|
2
|
+
import { dbConfigSchema } from "../types.js";
|
|
3
|
+
async function cli({
|
|
4
|
+
flags,
|
|
5
|
+
config: astroConfig
|
|
6
|
+
}) {
|
|
7
|
+
const args = flags._;
|
|
8
|
+
const command = args[2] === "db" ? args[3] : args[2];
|
|
9
|
+
const { mod } = await loadDbConfigFile(astroConfig.root);
|
|
10
|
+
const dbConfig = dbConfigSchema.parse(mod?.default ?? {});
|
|
11
|
+
switch (command) {
|
|
12
|
+
case "shell": {
|
|
13
|
+
const { cmd } = await import("./commands/shell/index.js");
|
|
14
|
+
return await cmd({ astroConfig, dbConfig, flags });
|
|
15
|
+
}
|
|
16
|
+
case "gen": {
|
|
17
|
+
console.log('"astro db gen" is no longer needed! Visit the docs for more information.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
case "sync": {
|
|
21
|
+
console.log('"astro db sync" is no longer needed! Visit the docs for more information.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
case "push": {
|
|
25
|
+
const { cmd } = await import("./commands/push/index.js");
|
|
26
|
+
return await cmd({ astroConfig, dbConfig, flags });
|
|
27
|
+
}
|
|
28
|
+
case "verify": {
|
|
29
|
+
const { cmd } = await import("./commands/verify/index.js");
|
|
30
|
+
return await cmd({ astroConfig, dbConfig, flags });
|
|
31
|
+
}
|
|
32
|
+
case "execute": {
|
|
33
|
+
const { cmd } = await import("./commands/execute/index.js");
|
|
34
|
+
return await cmd({ astroConfig, dbConfig, flags });
|
|
35
|
+
}
|
|
36
|
+
case "login": {
|
|
37
|
+
const { cmd } = await import("./commands/login/index.js");
|
|
38
|
+
return await cmd({ astroConfig, dbConfig, flags });
|
|
39
|
+
}
|
|
40
|
+
case "logout": {
|
|
41
|
+
const { cmd } = await import("./commands/logout/index.js");
|
|
42
|
+
return await cmd();
|
|
43
|
+
}
|
|
44
|
+
case "link": {
|
|
45
|
+
const { cmd } = await import("./commands/link/index.js");
|
|
46
|
+
return await cmd();
|
|
47
|
+
}
|
|
48
|
+
default: {
|
|
49
|
+
if (command == null) {
|
|
50
|
+
console.error(`No command provided.
|
|
51
|
+
|
|
52
|
+
${showHelp()}`);
|
|
53
|
+
} else {
|
|
54
|
+
console.error(`Unknown command: ${command}
|
|
55
|
+
|
|
56
|
+
${showHelp()}`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function showHelp() {
|
|
62
|
+
return `astro db <command>
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
|
|
66
|
+
astro login Authenticate your machine with Astro Studio
|
|
67
|
+
astro logout End your authenticated session with Astro Studio
|
|
68
|
+
astro link Link this directory to an Astro Studio project
|
|
69
|
+
|
|
70
|
+
astro db gen Creates snapshot based on your schema
|
|
71
|
+
astro db push Pushes schema updates to Astro Studio
|
|
72
|
+
astro db verify Tests schema updates /w Astro Studio (good for CI)`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export {
|
|
76
|
+
cli
|
|
77
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DBConfig, type DBSnapshot, type DBTable } from '../types.js';
|
|
2
|
+
export declare function getMigrationQueries({ oldSnapshot, newSnapshot, }: {
|
|
3
|
+
oldSnapshot: DBSnapshot;
|
|
4
|
+
newSnapshot: DBSnapshot;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
queries: string[];
|
|
7
|
+
confirmations: string[];
|
|
8
|
+
}>;
|
|
9
|
+
export declare function getCollectionChangeQueries({ collectionName, oldCollection, newCollection, }: {
|
|
10
|
+
collectionName: string;
|
|
11
|
+
oldCollection: DBTable;
|
|
12
|
+
newCollection: DBTable;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
queries: string[];
|
|
15
|
+
confirmations: string[];
|
|
16
|
+
}>;
|
|
17
|
+
export declare function getProductionCurrentSnapshot({ appToken, }: {
|
|
18
|
+
appToken: string;
|
|
19
|
+
}): Promise<DBSnapshot>;
|
|
20
|
+
export declare function createCurrentSnapshot({ tables }: DBConfig): DBSnapshot;
|
|
21
|
+
export declare function createEmptySnapshot(): DBSnapshot;
|
|
22
|
+
export declare function formatDataLossMessage(confirmations: string[], isColor?: boolean): string;
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import deepDiff from "deep-diff";
|
|
2
|
+
import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core";
|
|
3
|
+
import * as color from "kleur/colors";
|
|
4
|
+
import { customAlphabet } from "nanoid";
|
|
5
|
+
import stripAnsi from "strip-ansi";
|
|
6
|
+
import { hasPrimaryKey } from "../../runtime/index.js";
|
|
7
|
+
import {
|
|
8
|
+
getCreateIndexQueries,
|
|
9
|
+
getCreateTableQuery,
|
|
10
|
+
getDropTableIfExistsQuery,
|
|
11
|
+
getModifiers,
|
|
12
|
+
getReferencesConfig,
|
|
13
|
+
hasDefault,
|
|
14
|
+
schemaTypeToSqlType
|
|
15
|
+
} from "../../runtime/queries.js";
|
|
16
|
+
import { isSerializedSQL } from "../../runtime/types.js";
|
|
17
|
+
import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from "../errors.js";
|
|
18
|
+
import {
|
|
19
|
+
columnSchema
|
|
20
|
+
} from "../types.js";
|
|
21
|
+
import { getRemoteDatabaseUrl } from "../utils.js";
|
|
22
|
+
const sqlite = new SQLiteAsyncDialect();
|
|
23
|
+
const genTempTableName = customAlphabet("abcdefghijklmnopqrstuvwxyz", 10);
|
|
24
|
+
async function getMigrationQueries({
|
|
25
|
+
oldSnapshot,
|
|
26
|
+
newSnapshot
|
|
27
|
+
}) {
|
|
28
|
+
const queries = [];
|
|
29
|
+
const confirmations = [];
|
|
30
|
+
const addedCollections = getAddedCollections(oldSnapshot, newSnapshot);
|
|
31
|
+
const droppedTables = getDroppedCollections(oldSnapshot, newSnapshot);
|
|
32
|
+
const notDeprecatedDroppedTables = Object.fromEntries(
|
|
33
|
+
Object.entries(droppedTables).filter(([, table]) => !table.deprecated)
|
|
34
|
+
);
|
|
35
|
+
if (!isEmpty(addedCollections) && !isEmpty(notDeprecatedDroppedTables)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
RENAME_TABLE_ERROR(
|
|
38
|
+
Object.keys(addedCollections)[0],
|
|
39
|
+
Object.keys(notDeprecatedDroppedTables)[0]
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
for (const [collectionName, collection] of Object.entries(addedCollections)) {
|
|
44
|
+
queries.push(getDropTableIfExistsQuery(collectionName));
|
|
45
|
+
queries.push(getCreateTableQuery(collectionName, collection));
|
|
46
|
+
queries.push(...getCreateIndexQueries(collectionName, collection));
|
|
47
|
+
}
|
|
48
|
+
for (const [collectionName] of Object.entries(droppedTables)) {
|
|
49
|
+
const dropQuery = `DROP TABLE ${sqlite.escapeName(collectionName)}`;
|
|
50
|
+
queries.push(dropQuery);
|
|
51
|
+
}
|
|
52
|
+
for (const [collectionName, newCollection] of Object.entries(newSnapshot.schema)) {
|
|
53
|
+
const oldCollection = oldSnapshot.schema[collectionName];
|
|
54
|
+
if (!oldCollection)
|
|
55
|
+
continue;
|
|
56
|
+
const addedColumns = getAdded(oldCollection.columns, newCollection.columns);
|
|
57
|
+
const droppedColumns = getDropped(oldCollection.columns, newCollection.columns);
|
|
58
|
+
const notDeprecatedDroppedColumns = Object.fromEntries(
|
|
59
|
+
Object.entries(droppedColumns).filter(([key, col]) => !col.schema.deprecated)
|
|
60
|
+
);
|
|
61
|
+
if (!isEmpty(addedColumns) && !isEmpty(notDeprecatedDroppedColumns)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
RENAME_COLUMN_ERROR(
|
|
64
|
+
`${collectionName}.${Object.keys(addedColumns)[0]}`,
|
|
65
|
+
`${collectionName}.${Object.keys(notDeprecatedDroppedColumns)[0]}`
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const result = await getCollectionChangeQueries({
|
|
70
|
+
collectionName,
|
|
71
|
+
oldCollection,
|
|
72
|
+
newCollection
|
|
73
|
+
});
|
|
74
|
+
queries.push(...result.queries);
|
|
75
|
+
confirmations.push(...result.confirmations);
|
|
76
|
+
}
|
|
77
|
+
return { queries, confirmations };
|
|
78
|
+
}
|
|
79
|
+
async function getCollectionChangeQueries({
|
|
80
|
+
collectionName,
|
|
81
|
+
oldCollection,
|
|
82
|
+
newCollection
|
|
83
|
+
}) {
|
|
84
|
+
const queries = [];
|
|
85
|
+
const confirmations = [];
|
|
86
|
+
const updated = getUpdatedColumns(oldCollection.columns, newCollection.columns);
|
|
87
|
+
const added = getAdded(oldCollection.columns, newCollection.columns);
|
|
88
|
+
const dropped = getDropped(oldCollection.columns, newCollection.columns);
|
|
89
|
+
const hasForeignKeyChanges = Boolean(
|
|
90
|
+
deepDiff(oldCollection.foreignKeys, newCollection.foreignKeys)
|
|
91
|
+
);
|
|
92
|
+
if (!hasForeignKeyChanges && isEmpty(updated) && isEmpty(added) && isEmpty(dropped)) {
|
|
93
|
+
return {
|
|
94
|
+
queries: getChangeIndexQueries({
|
|
95
|
+
collectionName,
|
|
96
|
+
oldIndexes: oldCollection.indexes,
|
|
97
|
+
newIndexes: newCollection.indexes
|
|
98
|
+
}),
|
|
99
|
+
confirmations
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (!hasForeignKeyChanges && isEmpty(updated) && Object.values(dropped).every(canAlterTableDropColumn) && Object.values(added).every(canAlterTableAddColumn)) {
|
|
103
|
+
queries.push(
|
|
104
|
+
...getAlterTableQueries(collectionName, added, dropped),
|
|
105
|
+
...getChangeIndexQueries({
|
|
106
|
+
collectionName,
|
|
107
|
+
oldIndexes: oldCollection.indexes,
|
|
108
|
+
newIndexes: newCollection.indexes
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
return { queries, confirmations };
|
|
112
|
+
}
|
|
113
|
+
const dataLossCheck = canRecreateTableWithoutDataLoss(added, updated);
|
|
114
|
+
if (dataLossCheck.dataLoss) {
|
|
115
|
+
const { reason, columnName } = dataLossCheck;
|
|
116
|
+
const reasonMsgs = {
|
|
117
|
+
"added-required": `You added new required column '${color.bold(
|
|
118
|
+
collectionName + "." + columnName
|
|
119
|
+
)}' with no default value.
|
|
120
|
+
This cannot be executed on an existing table.`,
|
|
121
|
+
"updated-type": `Updating existing column ${color.bold(
|
|
122
|
+
collectionName + "." + columnName
|
|
123
|
+
)} to a new type that cannot be handled automatically.`
|
|
124
|
+
};
|
|
125
|
+
confirmations.push(reasonMsgs[reason]);
|
|
126
|
+
}
|
|
127
|
+
const primaryKeyExists = Object.entries(newCollection.columns).find(
|
|
128
|
+
([, column]) => hasPrimaryKey(column)
|
|
129
|
+
);
|
|
130
|
+
const droppedPrimaryKey = Object.entries(dropped).find(([, column]) => hasPrimaryKey(column));
|
|
131
|
+
const recreateTableQueries = getRecreateTableQueries({
|
|
132
|
+
collectionName,
|
|
133
|
+
newCollection,
|
|
134
|
+
added,
|
|
135
|
+
hasDataLoss: dataLossCheck.dataLoss,
|
|
136
|
+
migrateHiddenPrimaryKey: !primaryKeyExists && !droppedPrimaryKey
|
|
137
|
+
});
|
|
138
|
+
queries.push(...recreateTableQueries, ...getCreateIndexQueries(collectionName, newCollection));
|
|
139
|
+
return { queries, confirmations };
|
|
140
|
+
}
|
|
141
|
+
function getChangeIndexQueries({
|
|
142
|
+
collectionName,
|
|
143
|
+
oldIndexes = {},
|
|
144
|
+
newIndexes = {}
|
|
145
|
+
}) {
|
|
146
|
+
const added = getAdded(oldIndexes, newIndexes);
|
|
147
|
+
const dropped = getDropped(oldIndexes, newIndexes);
|
|
148
|
+
const updated = getUpdated(oldIndexes, newIndexes);
|
|
149
|
+
Object.assign(dropped, updated);
|
|
150
|
+
Object.assign(added, updated);
|
|
151
|
+
const queries = [];
|
|
152
|
+
for (const indexName of Object.keys(dropped)) {
|
|
153
|
+
const dropQuery = `DROP INDEX ${sqlite.escapeName(indexName)}`;
|
|
154
|
+
queries.push(dropQuery);
|
|
155
|
+
}
|
|
156
|
+
queries.push(...getCreateIndexQueries(collectionName, { indexes: added }));
|
|
157
|
+
return queries;
|
|
158
|
+
}
|
|
159
|
+
function getAddedCollections(oldCollections, newCollections) {
|
|
160
|
+
const added = {};
|
|
161
|
+
for (const [key, newCollection] of Object.entries(newCollections.schema)) {
|
|
162
|
+
if (!(key in oldCollections.schema))
|
|
163
|
+
added[key] = newCollection;
|
|
164
|
+
}
|
|
165
|
+
return added;
|
|
166
|
+
}
|
|
167
|
+
function getDroppedCollections(oldCollections, newCollections) {
|
|
168
|
+
const dropped = {};
|
|
169
|
+
for (const [key, oldCollection] of Object.entries(oldCollections.schema)) {
|
|
170
|
+
if (!(key in newCollections.schema))
|
|
171
|
+
dropped[key] = oldCollection;
|
|
172
|
+
}
|
|
173
|
+
return dropped;
|
|
174
|
+
}
|
|
175
|
+
function getAlterTableQueries(unescapedCollectionName, added, dropped) {
|
|
176
|
+
const queries = [];
|
|
177
|
+
const collectionName = sqlite.escapeName(unescapedCollectionName);
|
|
178
|
+
for (const [unescColumnName, column] of Object.entries(added)) {
|
|
179
|
+
const columnName = sqlite.escapeName(unescColumnName);
|
|
180
|
+
const type = schemaTypeToSqlType(column.type);
|
|
181
|
+
const q = `ALTER TABLE ${collectionName} ADD COLUMN ${columnName} ${type}${getModifiers(
|
|
182
|
+
columnName,
|
|
183
|
+
column
|
|
184
|
+
)}`;
|
|
185
|
+
queries.push(q);
|
|
186
|
+
}
|
|
187
|
+
for (const unescColumnName of Object.keys(dropped)) {
|
|
188
|
+
const columnName = sqlite.escapeName(unescColumnName);
|
|
189
|
+
const q = `ALTER TABLE ${collectionName} DROP COLUMN ${columnName}`;
|
|
190
|
+
queries.push(q);
|
|
191
|
+
}
|
|
192
|
+
return queries;
|
|
193
|
+
}
|
|
194
|
+
function getRecreateTableQueries({
|
|
195
|
+
collectionName: unescCollectionName,
|
|
196
|
+
newCollection,
|
|
197
|
+
added,
|
|
198
|
+
hasDataLoss,
|
|
199
|
+
migrateHiddenPrimaryKey
|
|
200
|
+
}) {
|
|
201
|
+
const unescTempName = `${unescCollectionName}_${genTempTableName()}`;
|
|
202
|
+
const tempName = sqlite.escapeName(unescTempName);
|
|
203
|
+
const collectionName = sqlite.escapeName(unescCollectionName);
|
|
204
|
+
if (hasDataLoss) {
|
|
205
|
+
return [
|
|
206
|
+
`DROP TABLE ${collectionName}`,
|
|
207
|
+
getCreateTableQuery(unescCollectionName, newCollection)
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
const newColumns = [...Object.keys(newCollection.columns)];
|
|
211
|
+
if (migrateHiddenPrimaryKey) {
|
|
212
|
+
newColumns.unshift("_id");
|
|
213
|
+
}
|
|
214
|
+
const escapedColumns = newColumns.filter((i) => !(i in added)).map((c) => sqlite.escapeName(c)).join(", ");
|
|
215
|
+
return [
|
|
216
|
+
getCreateTableQuery(unescTempName, newCollection),
|
|
217
|
+
`INSERT INTO ${tempName} (${escapedColumns}) SELECT ${escapedColumns} FROM ${collectionName}`,
|
|
218
|
+
`DROP TABLE ${collectionName}`,
|
|
219
|
+
`ALTER TABLE ${tempName} RENAME TO ${collectionName}`
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
function isEmpty(obj) {
|
|
223
|
+
return Object.keys(obj).length === 0;
|
|
224
|
+
}
|
|
225
|
+
function canAlterTableAddColumn(column) {
|
|
226
|
+
if (column.schema.unique)
|
|
227
|
+
return false;
|
|
228
|
+
if (hasRuntimeDefault(column))
|
|
229
|
+
return false;
|
|
230
|
+
if (!column.schema.optional && !hasDefault(column))
|
|
231
|
+
return false;
|
|
232
|
+
if (hasPrimaryKey(column))
|
|
233
|
+
return false;
|
|
234
|
+
if (getReferencesConfig(column))
|
|
235
|
+
return false;
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
function canAlterTableDropColumn(column) {
|
|
239
|
+
if (column.schema.unique)
|
|
240
|
+
return false;
|
|
241
|
+
if (hasPrimaryKey(column))
|
|
242
|
+
return false;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
function canRecreateTableWithoutDataLoss(added, updated) {
|
|
246
|
+
for (const [columnName, a] of Object.entries(added)) {
|
|
247
|
+
if (hasPrimaryKey(a) && a.type !== "number" && !hasDefault(a)) {
|
|
248
|
+
return { dataLoss: true, columnName, reason: "added-required" };
|
|
249
|
+
}
|
|
250
|
+
if (!a.schema.optional && !hasDefault(a)) {
|
|
251
|
+
return { dataLoss: true, columnName, reason: "added-required" };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const [columnName, u] of Object.entries(updated)) {
|
|
255
|
+
if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new)) {
|
|
256
|
+
return { dataLoss: true, columnName, reason: "updated-type" };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { dataLoss: false };
|
|
260
|
+
}
|
|
261
|
+
function getAdded(oldObj, newObj) {
|
|
262
|
+
const added = {};
|
|
263
|
+
for (const [key, value] of Object.entries(newObj)) {
|
|
264
|
+
if (!(key in oldObj))
|
|
265
|
+
added[key] = value;
|
|
266
|
+
}
|
|
267
|
+
return added;
|
|
268
|
+
}
|
|
269
|
+
function getDropped(oldObj, newObj) {
|
|
270
|
+
const dropped = {};
|
|
271
|
+
for (const [key, value] of Object.entries(oldObj)) {
|
|
272
|
+
if (!(key in newObj))
|
|
273
|
+
dropped[key] = value;
|
|
274
|
+
}
|
|
275
|
+
return dropped;
|
|
276
|
+
}
|
|
277
|
+
function getUpdated(oldObj, newObj) {
|
|
278
|
+
const updated = {};
|
|
279
|
+
for (const [key, value] of Object.entries(newObj)) {
|
|
280
|
+
const oldValue = oldObj[key];
|
|
281
|
+
if (!oldValue)
|
|
282
|
+
continue;
|
|
283
|
+
if (deepDiff(oldValue, value))
|
|
284
|
+
updated[key] = value;
|
|
285
|
+
}
|
|
286
|
+
return updated;
|
|
287
|
+
}
|
|
288
|
+
function getUpdatedColumns(oldColumns, newColumns) {
|
|
289
|
+
const updated = {};
|
|
290
|
+
for (const [key, newColumn] of Object.entries(newColumns)) {
|
|
291
|
+
let oldColumn = oldColumns[key];
|
|
292
|
+
if (!oldColumn)
|
|
293
|
+
continue;
|
|
294
|
+
if (oldColumn.type !== newColumn.type && canChangeTypeWithoutQuery(oldColumn, newColumn)) {
|
|
295
|
+
const asNewColumn = columnSchema.safeParse({
|
|
296
|
+
type: newColumn.type,
|
|
297
|
+
schema: oldColumn.schema
|
|
298
|
+
});
|
|
299
|
+
if (asNewColumn.success) {
|
|
300
|
+
oldColumn = asNewColumn.data;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const diff = deepDiff(oldColumn, newColumn);
|
|
304
|
+
if (diff) {
|
|
305
|
+
updated[key] = { old: oldColumn, new: newColumn };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return updated;
|
|
309
|
+
}
|
|
310
|
+
const typeChangesWithoutQuery = [
|
|
311
|
+
{ from: "boolean", to: "number" },
|
|
312
|
+
{ from: "date", to: "text" },
|
|
313
|
+
{ from: "json", to: "text" }
|
|
314
|
+
];
|
|
315
|
+
function canChangeTypeWithoutQuery(oldColumn, newColumn) {
|
|
316
|
+
return typeChangesWithoutQuery.some(
|
|
317
|
+
({ from, to }) => oldColumn.type === from && newColumn.type === to
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
function hasRuntimeDefault(column) {
|
|
321
|
+
return !!(column.schema.default && isSerializedSQL(column.schema.default));
|
|
322
|
+
}
|
|
323
|
+
async function getProductionCurrentSnapshot({
|
|
324
|
+
appToken
|
|
325
|
+
}) {
|
|
326
|
+
const url = new URL("/db/schema", getRemoteDatabaseUrl());
|
|
327
|
+
const response = await fetch(url, {
|
|
328
|
+
method: "POST",
|
|
329
|
+
headers: new Headers({
|
|
330
|
+
Authorization: `Bearer ${appToken}`
|
|
331
|
+
})
|
|
332
|
+
});
|
|
333
|
+
const result = await response.json();
|
|
334
|
+
return result.data;
|
|
335
|
+
}
|
|
336
|
+
function createCurrentSnapshot({ tables = {} }) {
|
|
337
|
+
const schema = JSON.parse(JSON.stringify(tables));
|
|
338
|
+
return { experimentalVersion: 1, schema };
|
|
339
|
+
}
|
|
340
|
+
function createEmptySnapshot() {
|
|
341
|
+
return { experimentalVersion: 1, schema: {} };
|
|
342
|
+
}
|
|
343
|
+
function formatDataLossMessage(confirmations, isColor = true) {
|
|
344
|
+
const messages = [];
|
|
345
|
+
messages.push(color.red("\u2716 We found some schema changes that cannot be handled automatically:"));
|
|
346
|
+
messages.push(``);
|
|
347
|
+
messages.push(...confirmations.map((m, i) => color.red(` (${i + 1}) `) + m));
|
|
348
|
+
messages.push(``);
|
|
349
|
+
messages.push(`To resolve, revert these changes or update your schema, and re-run the command.`);
|
|
350
|
+
messages.push(
|
|
351
|
+
`You may also run 'astro db push --force-reset' to ignore all warnings and force-push your local database schema to production instead. All data will be lost and the database will be reset.`
|
|
352
|
+
);
|
|
353
|
+
let finalMessage = messages.join("\n");
|
|
354
|
+
if (!isColor) {
|
|
355
|
+
finalMessage = stripAnsi(finalMessage);
|
|
356
|
+
}
|
|
357
|
+
return finalMessage;
|
|
358
|
+
}
|
|
359
|
+
export {
|
|
360
|
+
createCurrentSnapshot,
|
|
361
|
+
createEmptySnapshot,
|
|
362
|
+
formatDataLossMessage,
|
|
363
|
+
getCollectionChangeQueries,
|
|
364
|
+
getMigrationQueries,
|
|
365
|
+
getProductionCurrentSnapshot
|
|
366
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const PACKAGE_NAME: any;
|
|
2
|
+
export declare const RUNTIME_IMPORT: string;
|
|
3
|
+
export declare const RUNTIME_CONFIG_IMPORT: string;
|
|
4
|
+
export declare const DB_TYPES_FILE = "db-types.d.ts";
|
|
5
|
+
export declare const VIRTUAL_MODULE_ID = "astro:db";
|
|
6
|
+
export declare const DB_PATH = ".astro/content.db";
|
|
7
|
+
export declare const CONFIG_FILE_NAMES: string[];
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
const PACKAGE_NAME = JSON.parse(
|
|
3
|
+
readFileSync(new URL("../../package.json", import.meta.url), "utf8")
|
|
4
|
+
).name;
|
|
5
|
+
const RUNTIME_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime`);
|
|
6
|
+
const RUNTIME_CONFIG_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime/config`);
|
|
7
|
+
const DB_TYPES_FILE = "db-types.d.ts";
|
|
8
|
+
const VIRTUAL_MODULE_ID = "astro:db";
|
|
9
|
+
const DB_PATH = ".astro/content.db";
|
|
10
|
+
const CONFIG_FILE_NAMES = ["config.ts", "config.js", "config.mts", "config.mjs"];
|
|
11
|
+
export {
|
|
12
|
+
CONFIG_FILE_NAMES,
|
|
13
|
+
DB_PATH,
|
|
14
|
+
DB_TYPES_FILE,
|
|
15
|
+
PACKAGE_NAME,
|
|
16
|
+
RUNTIME_CONFIG_IMPORT,
|
|
17
|
+
RUNTIME_IMPORT,
|
|
18
|
+
VIRTUAL_MODULE_ID
|
|
19
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const MISSING_SESSION_ID_ERROR: string;
|
|
2
|
+
export declare const MISSING_PROJECT_ID_ERROR: string;
|
|
3
|
+
export declare const MISSING_EXECUTE_PATH_ERROR: string;
|
|
4
|
+
export declare const RENAME_TABLE_ERROR: (oldTable: string, newTable: string) => string;
|
|
5
|
+
export declare const RENAME_COLUMN_ERROR: (oldSelector: string, newSelector: string) => string;
|
|
6
|
+
export declare const FILE_NOT_FOUND_ERROR: (path: string) => string;
|
|
7
|
+
export declare const SEED_ERROR: (error: string) => string;
|
|
8
|
+
export declare const REFERENCE_DNE_ERROR: (columnName: string) => string;
|
|
9
|
+
export declare const FOREIGN_KEY_DNE_ERROR: (tableName: string) => string;
|
|
10
|
+
export declare const FOREIGN_KEY_REFERENCES_LENGTH_ERROR: (tableName: string) => string;
|
|
11
|
+
export declare const FOREIGN_KEY_REFERENCES_EMPTY_ERROR: (tableName: string) => string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { bold, cyan, green, red, yellow } from "kleur/colors";
|
|
2
|
+
const MISSING_SESSION_ID_ERROR = `${red("\u25B6 Login required!")}
|
|
3
|
+
|
|
4
|
+
To authenticate with Astro Studio, run
|
|
5
|
+
${cyan("astro db login")}
|
|
6
|
+
`;
|
|
7
|
+
const MISSING_PROJECT_ID_ERROR = `${red("\u25B6 Directory not linked.")}
|
|
8
|
+
|
|
9
|
+
To link this directory to an Astro Studio project, run
|
|
10
|
+
${cyan("astro db link")}
|
|
11
|
+
`;
|
|
12
|
+
const MISSING_EXECUTE_PATH_ERROR = `${red(
|
|
13
|
+
"\u25B6 No file path provided."
|
|
14
|
+
)} Provide a path by running ${cyan("astro db execute <path>")}
|
|
15
|
+
`;
|
|
16
|
+
const RENAME_TABLE_ERROR = (oldTable, newTable) => {
|
|
17
|
+
return red("\u25B6 Potential table rename detected: " + oldTable + ", " + newTable) + `
|
|
18
|
+
You cannot add and remove tables in the same schema update batch.
|
|
19
|
+
To resolve, add a 'deprecated: true' flag to '${oldTable}' instead.`;
|
|
20
|
+
};
|
|
21
|
+
const RENAME_COLUMN_ERROR = (oldSelector, newSelector) => {
|
|
22
|
+
return red("\u25B6 Potential column rename detected: " + oldSelector + ", " + newSelector) + `
|
|
23
|
+
You cannot add and remove columns in the same table.
|
|
24
|
+
To resolve, add a 'deprecated: true' flag to '${oldSelector}' instead.`;
|
|
25
|
+
};
|
|
26
|
+
const FILE_NOT_FOUND_ERROR = (path) => `${red("\u25B6 File not found:")} ${bold(path)}
|
|
27
|
+
`;
|
|
28
|
+
const SEED_ERROR = (error) => {
|
|
29
|
+
return `${red(`Error while seeding database:`)}
|
|
30
|
+
|
|
31
|
+
${error}`;
|
|
32
|
+
};
|
|
33
|
+
const REFERENCE_DNE_ERROR = (columnName) => {
|
|
34
|
+
return `Column ${bold(
|
|
35
|
+
columnName
|
|
36
|
+
)} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
|
|
37
|
+
};
|
|
38
|
+
const FOREIGN_KEY_DNE_ERROR = (tableName) => {
|
|
39
|
+
return `Table ${bold(
|
|
40
|
+
tableName
|
|
41
|
+
)} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
|
|
42
|
+
};
|
|
43
|
+
const FOREIGN_KEY_REFERENCES_LENGTH_ERROR = (tableName) => {
|
|
44
|
+
return `Foreign key on ${bold(
|
|
45
|
+
tableName
|
|
46
|
+
)} is misconfigured. \`columns\` and \`references\` must be the same length.`;
|
|
47
|
+
};
|
|
48
|
+
const FOREIGN_KEY_REFERENCES_EMPTY_ERROR = (tableName) => {
|
|
49
|
+
return `Foreign key on ${bold(
|
|
50
|
+
tableName
|
|
51
|
+
)} is misconfigured. \`references\` array cannot be empty.`;
|
|
52
|
+
};
|
|
53
|
+
export {
|
|
54
|
+
FILE_NOT_FOUND_ERROR,
|
|
55
|
+
FOREIGN_KEY_DNE_ERROR,
|
|
56
|
+
FOREIGN_KEY_REFERENCES_EMPTY_ERROR,
|
|
57
|
+
FOREIGN_KEY_REFERENCES_LENGTH_ERROR,
|
|
58
|
+
MISSING_EXECUTE_PATH_ERROR,
|
|
59
|
+
MISSING_PROJECT_ID_ERROR,
|
|
60
|
+
MISSING_SESSION_ID_ERROR,
|
|
61
|
+
REFERENCE_DNE_ERROR,
|
|
62
|
+
RENAME_COLUMN_ERROR,
|
|
63
|
+
RENAME_TABLE_ERROR,
|
|
64
|
+
SEED_ERROR
|
|
65
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const errorMap = (baseError, ctx) => {
|
|
2
|
+
const baseErrorPath = flattenErrorPath(baseError.path);
|
|
3
|
+
if (baseError.code === "invalid_union") {
|
|
4
|
+
const typeOrLiteralErrByPath = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const unionError of baseError.unionErrors.flatMap((e) => e.errors)) {
|
|
6
|
+
if (unionError.code === "invalid_type" || unionError.code === "invalid_literal") {
|
|
7
|
+
const flattenedErrorPath = flattenErrorPath(unionError.path);
|
|
8
|
+
const typeOrLiteralErr = typeOrLiteralErrByPath.get(flattenedErrorPath);
|
|
9
|
+
if (typeOrLiteralErr) {
|
|
10
|
+
typeOrLiteralErr.expected.push(unionError.expected);
|
|
11
|
+
} else {
|
|
12
|
+
typeOrLiteralErrByPath.set(flattenedErrorPath, {
|
|
13
|
+
code: unionError.code,
|
|
14
|
+
received: unionError.received,
|
|
15
|
+
expected: [unionError.expected]
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const messages = [
|
|
21
|
+
prefix(
|
|
22
|
+
baseErrorPath,
|
|
23
|
+
typeOrLiteralErrByPath.size ? "Did not match union:" : "Did not match union."
|
|
24
|
+
)
|
|
25
|
+
];
|
|
26
|
+
return {
|
|
27
|
+
message: messages.concat(
|
|
28
|
+
[...typeOrLiteralErrByPath.entries()].filter(([, error]) => error.expected.length === baseError.unionErrors.length).map(
|
|
29
|
+
([key, error]) => (
|
|
30
|
+
// Avoid printing the key again if it's a base error
|
|
31
|
+
key === baseErrorPath ? `> ${getTypeOrLiteralMsg(error)}` : `> ${prefix(key, getTypeOrLiteralMsg(error))}`
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
).join("\n")
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (baseError.code === "invalid_literal" || baseError.code === "invalid_type") {
|
|
38
|
+
return {
|
|
39
|
+
message: prefix(
|
|
40
|
+
baseErrorPath,
|
|
41
|
+
getTypeOrLiteralMsg({
|
|
42
|
+
code: baseError.code,
|
|
43
|
+
received: baseError.received,
|
|
44
|
+
expected: [baseError.expected]
|
|
45
|
+
})
|
|
46
|
+
)
|
|
47
|
+
};
|
|
48
|
+
} else if (baseError.message) {
|
|
49
|
+
return { message: prefix(baseErrorPath, baseError.message) };
|
|
50
|
+
} else {
|
|
51
|
+
return { message: prefix(baseErrorPath, ctx.defaultError) };
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const getTypeOrLiteralMsg = (error) => {
|
|
55
|
+
if (error.received === "undefined")
|
|
56
|
+
return "Required";
|
|
57
|
+
const expectedDeduped = new Set(error.expected);
|
|
58
|
+
switch (error.code) {
|
|
59
|
+
case "invalid_type":
|
|
60
|
+
return `Expected type \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify(
|
|
61
|
+
error.received
|
|
62
|
+
)}`;
|
|
63
|
+
case "invalid_literal":
|
|
64
|
+
return `Expected \`${unionExpectedVals(expectedDeduped)}\`, received ${JSON.stringify(
|
|
65
|
+
error.received
|
|
66
|
+
)}`;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const prefix = (key, msg) => key.length ? `**${key}**: ${msg}` : msg;
|
|
70
|
+
const unionExpectedVals = (expectedVals) => [...expectedVals].map((expectedVal, idx) => {
|
|
71
|
+
if (idx === 0)
|
|
72
|
+
return JSON.stringify(expectedVal);
|
|
73
|
+
const sep = " | ";
|
|
74
|
+
return `${sep}${JSON.stringify(expectedVal)}`;
|
|
75
|
+
}).join("");
|
|
76
|
+
const flattenErrorPath = (errorPath) => errorPath.join(".");
|
|
77
|
+
export {
|
|
78
|
+
errorMap
|
|
79
|
+
};
|