@astrojs/db 0.1.2 → 0.1.4

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.
@@ -4,6 +4,8 @@ import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core";
4
4
  import { appTokenError } from "../../../errors.js";
5
5
  import { collectionToTable, createLocalDatabaseClient } from "../../../internal.js";
6
6
  import {
7
+ createCurrentSnapshot,
8
+ createEmptySnapshot,
7
9
  getMigrations,
8
10
  initializeFromMigrations,
9
11
  loadInitialSnapshot,
@@ -24,8 +26,7 @@ async function cmd({ config, flags }) {
24
26
  const isSeedData = flags.seed;
25
27
  const isDryRun = flags.dryRun;
26
28
  const appToken = flags.token ?? getAstroStudioEnv().ASTRO_STUDIO_APP_TOKEN;
27
- const currentDb = config.db?.collections ?? {};
28
- const currentSnapshot = JSON.parse(JSON.stringify(currentDb));
29
+ const currentSnapshot = createCurrentSnapshot(config);
29
30
  const allMigrationFiles = await getMigrations();
30
31
  if (allMigrationFiles.length === 0) {
31
32
  console.log("Project not yet initialized!");
@@ -35,6 +36,7 @@ async function cmd({ config, flags }) {
35
36
  const calculatedDiff = diff(prevSnapshot, currentSnapshot);
36
37
  if (calculatedDiff) {
37
38
  console.log("Changes detected!");
39
+ console.log(calculatedDiff);
38
40
  process.exit(1);
39
41
  }
40
42
  if (!appToken) {
@@ -58,7 +60,7 @@ async function cmd({ config, flags }) {
58
60
  }
59
61
  if (isSeedData) {
60
62
  console.info("Pushing data...");
61
- await tempDataPush({ currentDb, appToken, isDryRun });
63
+ await pushData({ config, appToken, isDryRun });
62
64
  }
63
65
  console.info("Push complete!");
64
66
  }
@@ -73,8 +75,8 @@ async function pushSchema({
73
75
  const filteredMigrations = migrations.filter((m) => m !== "0000_snapshot.json");
74
76
  const missingMigrationContents = await Promise.all(filteredMigrations.map(loadMigration));
75
77
  const initialMigrationBatch = initialSnapshot ? await getMigrationQueries({
76
- oldCollections: {},
77
- newCollections: await loadInitialSnapshot()
78
+ oldSnapshot: createEmptySnapshot(),
79
+ newSnapshot: await loadInitialSnapshot()
78
80
  }) : [];
79
81
  const missingMigrationBatch = missingMigrationContents.reduce((acc, curr) => {
80
82
  return [...acc, ...curr.db];
@@ -84,26 +86,24 @@ async function pushSchema({
84
86
  await db.insert(migrationsTable).values(migrations.map((m) => ({ name: m })));
85
87
  await db.update(adminTable).set({ collections: JSON.stringify(currentSnapshot) }).where(eq(adminTable.id, STUDIO_ADMIN_TABLE_ROW_ID));
86
88
  }
87
- async function tempDataPush({
88
- currentDb,
89
+ async function pushData({
90
+ config,
89
91
  appToken,
90
92
  isDryRun
91
93
  }) {
92
94
  const db = await createLocalDatabaseClient({
93
- collections: JSON.parse(JSON.stringify(currentDb)),
95
+ collections: config.db.collections,
94
96
  dbUrl: ":memory:",
95
97
  seeding: true
96
98
  });
97
99
  const queries = [];
98
- for (const [name, collection] of Object.entries(currentDb)) {
99
- console.log(name, collection);
100
+ for (const [name, collection] of Object.entries(config.db.collections)) {
100
101
  if (collection.writable || !collection.data)
101
102
  continue;
102
103
  const table = collectionToTable(name, collection);
103
104
  const insert = db.insert(table).values(await collection.data());
104
105
  queries.push(insert.toSQL());
105
106
  }
106
- console.log(queries);
107
107
  const url = new URL("/db/query", getRemoteDatabaseUrl());
108
108
  const requestBody = queries.map((q) => ({
109
109
  sql: q.sql,
@@ -0,0 +1,6 @@
1
+ import type { AstroConfig } from 'astro';
2
+ import type { Arguments } from 'yargs-parser';
3
+ export declare function cmd({ config, flags }: {
4
+ config: AstroConfig;
5
+ flags: Arguments;
6
+ }): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { appTokenError } from "../../../errors.js";
3
+ import { createRemoteDatabaseClient, getAstroStudioEnv } from "../../../utils.js";
4
+ async function cmd({ config, flags }) {
5
+ const query = flags.query;
6
+ const appToken = flags.token ?? getAstroStudioEnv().ASTRO_STUDIO_APP_TOKEN;
7
+ if (!appToken) {
8
+ console.error(appTokenError);
9
+ process.exit(1);
10
+ }
11
+ const db = createRemoteDatabaseClient(appToken);
12
+ const result = await db.run(sql.raw(query));
13
+ console.log(result);
14
+ }
15
+ export {
16
+ cmd
17
+ };
@@ -1,6 +1,7 @@
1
1
  import deepDiff from "deep-diff";
2
2
  import { writeFile } from "fs/promises";
3
3
  import {
4
+ createCurrentSnapshot,
4
5
  getMigrations,
5
6
  initializeFromMigrations,
6
7
  initializeMigrationsDirectory
@@ -8,7 +9,7 @@ import {
8
9
  import { getMigrationQueries } from "../../queries.js";
9
10
  const { diff } = deepDiff;
10
11
  async function cmd({ config }) {
11
- const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
12
+ const currentSnapshot = createCurrentSnapshot(config);
12
13
  const allMigrationFiles = await getMigrations();
13
14
  if (allMigrationFiles.length === 0) {
14
15
  await initializeMigrationsDirectory(currentSnapshot);
@@ -22,8 +23,8 @@ async function cmd({ config }) {
22
23
  return;
23
24
  }
24
25
  const migrationQueries = await getMigrationQueries({
25
- oldCollections: prevSnapshot,
26
- newCollections: currentSnapshot
26
+ oldSnapshot: prevSnapshot,
27
+ newSnapshot: currentSnapshot
27
28
  });
28
29
  const largestNumber = allMigrationFiles.reduce((acc, curr) => {
29
30
  const num = parseInt(curr.split("_")[0]);
@@ -1,9 +1,6 @@
1
1
  import deepDiff from "deep-diff";
2
- import {
3
- getMigrations,
4
- initializeFromMigrations
5
- } from "../../../migrations.js";
6
- const { diff, applyChange } = deepDiff;
2
+ import { getMigrations, initializeFromMigrations } from "../../../migrations.js";
3
+ const { diff } = deepDiff;
7
4
  async function cmd({ config }) {
8
5
  const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
9
6
  const allMigrationFiles = await getMigrations();
package/dist/cli/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  async function cli({ flags, config }) {
2
2
  const command = flags._[3];
3
3
  switch (command) {
4
+ case "shell": {
5
+ const { cmd: shellCommand } = await import("./commands/shell/index.js");
6
+ return await shellCommand({ config, flags });
7
+ }
4
8
  case "sync": {
5
9
  const { cmd: syncCommand } = await import("./commands/sync/index.js");
6
10
  return await syncCommand({ config, flags });
@@ -1,12 +1,12 @@
1
- import type { DBCollection, DBCollections } from '../types.js';
1
+ import type { DBCollection, DBSnapshot } from '../types.js';
2
2
  interface PromptResponses {
3
3
  allowDataLoss: boolean;
4
4
  fieldRenames: Record<string, string | false>;
5
5
  collectionRenames: Record<string, string | false>;
6
6
  }
7
- export declare function getMigrationQueries({ oldCollections, newCollections, promptResponses, }: {
8
- oldCollections: DBCollections;
9
- newCollections: DBCollections;
7
+ export declare function getMigrationQueries({ oldSnapshot, newSnapshot, promptResponses, }: {
8
+ oldSnapshot: DBSnapshot;
9
+ newSnapshot: DBSnapshot;
10
10
  promptResponses?: PromptResponses;
11
11
  }): Promise<string[]>;
12
12
  export declare function getCollectionChangeQueries({ collectionName, oldCollection, newCollection, promptResponses, }: {
@@ -15,5 +15,4 @@ export declare function getCollectionChangeQueries({ collectionName, oldCollecti
15
15
  newCollection: DBCollection;
16
16
  promptResponses?: PromptResponses;
17
17
  }): Promise<string[]>;
18
- export declare function getCreateTableQuery(collectionName: string, collection: DBCollection): string;
19
18
  export {};
@@ -2,16 +2,17 @@ import * as color from "kleur/colors";
2
2
  import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core";
3
3
  import { customAlphabet } from "nanoid";
4
4
  import prompts from "prompts";
5
+ import { getCreateTableQuery, getModifiers, hasDefault, hasPrimaryKey, schemaTypeToSqlType } from "../internal.js";
5
6
  const sqlite = new SQLiteAsyncDialect();
6
7
  const genTempTableName = customAlphabet("abcdefghijklmnopqrstuvwxyz", 10);
7
8
  async function getMigrationQueries({
8
- oldCollections,
9
- newCollections,
9
+ oldSnapshot,
10
+ newSnapshot,
10
11
  promptResponses
11
12
  }) {
12
13
  const queries = [];
13
- let added = getAddedCollections(oldCollections, newCollections);
14
- let dropped = getDroppedCollections(oldCollections, newCollections);
14
+ let added = getAddedCollections(oldSnapshot, newSnapshot);
15
+ let dropped = getDroppedCollections(oldSnapshot, newSnapshot);
15
16
  if (!isEmpty(added) && !isEmpty(dropped)) {
16
17
  const resolved = await resolveCollectionRenames(
17
18
  added,
@@ -34,8 +35,8 @@ async function getMigrationQueries({
34
35
  const dropQuery = `DROP TABLE ${sqlite.escapeName(collectionName)}`;
35
36
  queries.push(dropQuery);
36
37
  }
37
- for (const [collectionName, newCollection] of Object.entries(newCollections)) {
38
- const oldCollection = oldCollections[collectionName];
38
+ for (const [collectionName, newCollection] of Object.entries(newSnapshot.schema)) {
39
+ const oldCollection = oldSnapshot.schema[collectionName];
39
40
  if (!oldCollection)
40
41
  continue;
41
42
  const collectionChangeQueries = await getCollectionChangeQueries({
@@ -89,12 +90,9 @@ async function getCollectionChangeQueries({
89
90
  )} field ${color.blue(color.bold(fieldName))}. ${color.red(
90
91
  "This will delete all existing data in the collection!"
91
92
  )} We recommend setting a default value to avoid data loss.`,
92
- "updated-required": `Changing ${color.blue(color.bold(collectionName))} field ${color.blue(
93
+ "added-unique": `Adding unique ${color.blue(color.bold(collectionName))} field ${color.blue(
93
94
  color.bold(fieldName)
94
- )} to required. ${color.red("This will delete all existing data in the collection!")}`,
95
- "updated-unique": `Changing ${color.blue(color.bold(collectionName))} field ${color.blue(
96
- color.bold(fieldName)
97
- )} to unique. ${color.red("This will delete all existing data in the collection!")}`,
95
+ )}. ${color.red("This will delete all existing data in the collection!")}`,
98
96
  "updated-type": `Changing the type of ${color.blue(
99
97
  color.bold(collectionName)
100
98
  )} field ${color.blue(color.bold(fieldName))}. ${color.red(
@@ -117,11 +115,21 @@ async function getCollectionChangeQueries({
117
115
  process.exit(0);
118
116
  }
119
117
  }
118
+ const addedPrimaryKey = Object.entries(added).find(
119
+ ([, field]) => hasPrimaryKey(field)
120
+ );
121
+ const droppedPrimaryKey = Object.entries(dropped).find(
122
+ ([, field]) => hasPrimaryKey(field)
123
+ );
124
+ const updatedPrimaryKey = Object.entries(updated).find(
125
+ ([, field]) => hasPrimaryKey(field.old) || hasPrimaryKey(field.new)
126
+ );
120
127
  const recreateTableQueries = getRecreateTableQueries({
121
- unescCollectionName: collectionName,
128
+ unescapedCollectionName: collectionName,
122
129
  newCollection,
123
130
  added,
124
- hasDataLoss: dataLossCheck.dataLoss
131
+ hasDataLoss: dataLossCheck.dataLoss,
132
+ migrateHiddenPrimaryKey: !addedPrimaryKey && !droppedPrimaryKey && !updatedPrimaryKey
125
133
  });
126
134
  queries.push(...recreateTableQueries);
127
135
  return queries;
@@ -222,23 +230,23 @@ async function resolveCollectionRenames(mightAdd, mightDrop, renamePromptRespons
222
230
  }
223
231
  function getAddedCollections(oldCollections, newCollections) {
224
232
  const added = {};
225
- for (const [key, newCollection] of Object.entries(newCollections)) {
226
- if (!(key in oldCollections))
233
+ for (const [key, newCollection] of Object.entries(newCollections.schema)) {
234
+ if (!(key in oldCollections.schema))
227
235
  added[key] = newCollection;
228
236
  }
229
237
  return added;
230
238
  }
231
239
  function getDroppedCollections(oldCollections, newCollections) {
232
240
  const dropped = {};
233
- for (const [key, oldCollection] of Object.entries(oldCollections)) {
234
- if (!(key in newCollections))
241
+ for (const [key, oldCollection] of Object.entries(oldCollections.schema)) {
242
+ if (!(key in newCollections.schema))
235
243
  dropped[key] = oldCollection;
236
244
  }
237
245
  return dropped;
238
246
  }
239
- function getFieldRenameQueries(unescCollectionName, renamed) {
247
+ function getFieldRenameQueries(unescapedCollectionName, renamed) {
240
248
  const queries = [];
241
- const collectionName = sqlite.escapeName(unescCollectionName);
249
+ const collectionName = sqlite.escapeName(unescapedCollectionName);
242
250
  for (const { from, to } of renamed) {
243
251
  const q = `ALTER TABLE ${collectionName} RENAME COLUMN ${sqlite.escapeName(
244
252
  from
@@ -247,9 +255,9 @@ function getFieldRenameQueries(unescCollectionName, renamed) {
247
255
  }
248
256
  return queries;
249
257
  }
250
- function getAlterTableQueries(unescCollectionName, added, dropped) {
258
+ function getAlterTableQueries(unescapedCollectionName, added, dropped) {
251
259
  const queries = [];
252
- const collectionName = sqlite.escapeName(unescCollectionName);
260
+ const collectionName = sqlite.escapeName(unescapedCollectionName);
253
261
  for (const [unescFieldName, field] of Object.entries(added)) {
254
262
  const fieldName = sqlite.escapeName(unescFieldName);
255
263
  const type = schemaTypeToSqlType(field.type);
@@ -267,92 +275,67 @@ function getAlterTableQueries(unescCollectionName, added, dropped) {
267
275
  return queries;
268
276
  }
269
277
  function getRecreateTableQueries({
270
- unescCollectionName,
278
+ unescapedCollectionName,
271
279
  newCollection,
272
280
  added,
273
- hasDataLoss
281
+ hasDataLoss,
282
+ migrateHiddenPrimaryKey
274
283
  }) {
275
- const unescTempName = `${unescCollectionName}_${genTempTableName()}`;
284
+ const unescTempName = `${unescapedCollectionName}_${genTempTableName()}`;
276
285
  const tempName = sqlite.escapeName(unescTempName);
277
- const collectionName = sqlite.escapeName(unescCollectionName);
278
- const queries = [getCreateTableQuery(unescTempName, newCollection)];
279
- if (!hasDataLoss) {
280
- const newColumns = ["id", ...Object.keys(newCollection.fields)];
281
- const originalColumns = newColumns.filter((i) => !(i in added));
282
- const escapedColumns = originalColumns.map((c) => sqlite.escapeName(c)).join(", ");
283
- queries.push(
284
- `INSERT INTO ${tempName} (${escapedColumns}) SELECT ${escapedColumns} FROM ${collectionName}`
285
- );
286
+ const collectionName = sqlite.escapeName(unescapedCollectionName);
287
+ if (hasDataLoss) {
288
+ return [`DROP TABLE ${collectionName}`, getCreateTableQuery(collectionName, newCollection)];
286
289
  }
287
- queries.push(`DROP TABLE ${collectionName}`);
288
- queries.push(`ALTER TABLE ${tempName} RENAME TO ${collectionName}`);
289
- return queries;
290
- }
291
- function getCreateTableQuery(collectionName, collection) {
292
- let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
293
- const colQueries = ['"id" text PRIMARY KEY'];
294
- for (const [columnName, column] of Object.entries(collection.fields)) {
295
- const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
296
- column.type
297
- )}${getModifiers(columnName, column)}`;
298
- colQueries.push(colQuery);
299
- }
300
- query += colQueries.join(", ") + ")";
301
- return query;
302
- }
303
- function getModifiers(columnName, column) {
304
- let modifiers = "";
305
- if (!column.optional) {
306
- modifiers += " NOT NULL";
307
- }
308
- if (column.unique) {
309
- modifiers += " UNIQUE";
310
- }
311
- if (hasDefault(column)) {
312
- modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`;
313
- }
314
- return modifiers;
315
- }
316
- function schemaTypeToSqlType(type) {
317
- switch (type) {
318
- case "date":
319
- case "text":
320
- case "json":
321
- return "text";
322
- case "number":
323
- case "boolean":
324
- return "integer";
290
+ const newColumns = [...Object.keys(newCollection.fields)];
291
+ if (migrateHiddenPrimaryKey) {
292
+ newColumns.unshift("_id");
325
293
  }
294
+ const escapedColumns = newColumns.filter((i) => !(i in added)).map((c) => sqlite.escapeName(c)).join(", ");
295
+ return [
296
+ getCreateTableQuery(unescTempName, newCollection),
297
+ `INSERT INTO ${tempName} (${escapedColumns}) SELECT ${escapedColumns} FROM ${collectionName}`,
298
+ `DROP TABLE ${collectionName}`,
299
+ `ALTER TABLE ${tempName} RENAME TO ${collectionName}`
300
+ ];
326
301
  }
327
302
  function isEmpty(obj) {
328
303
  return Object.keys(obj).length === 0;
329
304
  }
330
- function canAlterTableAddColumn(column) {
331
- if (column.unique)
305
+ function canAlterTableAddColumn(field) {
306
+ if (field.unique)
332
307
  return false;
333
- if (hasRuntimeDefault(column))
308
+ if (hasRuntimeDefault(field))
334
309
  return false;
335
- if (!column.optional && !hasDefault(column))
310
+ if (!field.optional && !hasDefault(field))
311
+ return false;
312
+ if (hasPrimaryKey(field))
336
313
  return false;
337
314
  return true;
338
315
  }
339
- function canAlterTableDropColumn(column) {
340
- if (column.unique)
316
+ function canAlterTableDropColumn(field) {
317
+ if (field.unique)
318
+ return false;
319
+ if (hasPrimaryKey(field))
341
320
  return false;
342
321
  return true;
343
322
  }
344
323
  function canRecreateTableWithoutDataLoss(added, updated) {
345
324
  for (const [fieldName, a] of Object.entries(added)) {
346
- if (!a.optional && !hasDefault(a))
325
+ if (hasPrimaryKey(a) && a.type !== "number" && !hasDefault(a)) {
326
+ return { dataLoss: true, fieldName, reason: "added-required" };
327
+ }
328
+ if (!a.optional && !hasDefault(a)) {
347
329
  return { dataLoss: true, fieldName, reason: "added-required" };
330
+ }
331
+ if (a.unique) {
332
+ return { dataLoss: true, fieldName, reason: "added-unique" };
333
+ }
348
334
  }
349
335
  for (const [fieldName, u] of Object.entries(updated)) {
350
- if (u.old.optional && !u.new.optional)
351
- return { dataLoss: true, fieldName, reason: "updated-required" };
352
- if (!u.old.unique && u.new.unique)
353
- return { dataLoss: true, fieldName, reason: "updated-unique" };
354
- if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new))
336
+ if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new)) {
355
337
  return { dataLoss: true, fieldName, reason: "updated-type" };
338
+ }
356
339
  }
357
340
  return { dataLoss: false };
358
341
  }
@@ -404,38 +387,9 @@ function canChangeTypeWithoutQuery(oldField, newField) {
404
387
  ({ from, to }) => oldField.type === from && newField.type === to
405
388
  );
406
389
  }
407
- function hasDefault(field) {
408
- return field.default !== void 0;
409
- }
410
390
  function hasRuntimeDefault(field) {
411
391
  return field.type === "date" && field.default === "now";
412
392
  }
413
- function getDefaultValueSql(columnName, column) {
414
- switch (column.type) {
415
- case "boolean":
416
- return column.default ? "TRUE" : "FALSE";
417
- case "number":
418
- return `${column.default}`;
419
- case "text":
420
- return sqlite.escapeString(column.default);
421
- case "date":
422
- return column.default === "now" ? "CURRENT_TIMESTAMP" : sqlite.escapeString(column.default);
423
- case "json": {
424
- let stringified = "";
425
- try {
426
- stringified = JSON.stringify(column.default);
427
- } catch (e) {
428
- console.log(
429
- `Invalid default value for column ${color.bold(
430
- columnName
431
- )}. Defaults must be valid JSON when using the \`json()\` type.`
432
- );
433
- process.exit(0);
434
- }
435
- return sqlite.escapeString(stringified);
436
- }
437
- }
438
- }
439
393
  function objShallowEqual(a, b) {
440
394
  if (Object.keys(a).length !== Object.keys(b).length)
441
395
  return false;
@@ -448,6 +402,5 @@ function objShallowEqual(a, b) {
448
402
  }
449
403
  export {
450
404
  getCollectionChangeQueries,
451
- getCreateTableQuery,
452
405
  getMigrationQueries
453
406
  };