mirror of
https://github.com/withastro/astro.git
synced 2025-02-10 22:38:53 -05:00
add primaryKey support
This commit is contained in:
parent
65696de832
commit
7c786276f5
8 changed files with 144 additions and 296 deletions
|
@ -7,6 +7,7 @@ import type { Arguments } from 'yargs-parser';
|
||||||
import { appTokenError } from '../../../errors.js';
|
import { appTokenError } from '../../../errors.js';
|
||||||
import { collectionToTable, createLocalDatabaseClient } from '../../../internal.js';
|
import { collectionToTable, createLocalDatabaseClient } from '../../../internal.js';
|
||||||
import {
|
import {
|
||||||
|
createEmptySnapshot,
|
||||||
getMigrations,
|
getMigrations,
|
||||||
initializeFromMigrations,
|
initializeFromMigrations,
|
||||||
loadInitialSnapshot,
|
loadInitialSnapshot,
|
||||||
|
@ -98,8 +99,8 @@ async function pushSchema({
|
||||||
// create a migration for the initial snapshot, if needed
|
// create a migration for the initial snapshot, if needed
|
||||||
const initialMigrationBatch = initialSnapshot
|
const initialMigrationBatch = initialSnapshot
|
||||||
? await getMigrationQueries({
|
? await getMigrationQueries({
|
||||||
oldCollections: {},
|
oldSnapshot: createEmptySnapshot(),
|
||||||
newCollections: await loadInitialSnapshot(),
|
newSnapshot: await loadInitialSnapshot(),
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
// combine all missing migrations into a single batch
|
// combine all missing migrations into a single batch
|
||||||
|
|
|
@ -3,6 +3,7 @@ import deepDiff from 'deep-diff';
|
||||||
import { writeFile } from 'fs/promises';
|
import { writeFile } from 'fs/promises';
|
||||||
import type { Arguments } from 'yargs-parser';
|
import type { Arguments } from 'yargs-parser';
|
||||||
import {
|
import {
|
||||||
|
createCurrentSnapshot,
|
||||||
getMigrations,
|
getMigrations,
|
||||||
initializeFromMigrations,
|
initializeFromMigrations,
|
||||||
initializeMigrationsDirectory,
|
initializeMigrationsDirectory,
|
||||||
|
@ -11,7 +12,7 @@ import { getMigrationQueries } from '../../queries.js';
|
||||||
const { diff } = deepDiff;
|
const { diff } = deepDiff;
|
||||||
|
|
||||||
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
|
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
|
||||||
const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
|
const currentSnapshot = createCurrentSnapshot(config);
|
||||||
const allMigrationFiles = await getMigrations();
|
const allMigrationFiles = await getMigrations();
|
||||||
if (allMigrationFiles.length === 0) {
|
if (allMigrationFiles.length === 0) {
|
||||||
await initializeMigrationsDirectory(currentSnapshot);
|
await initializeMigrationsDirectory(currentSnapshot);
|
||||||
|
@ -27,8 +28,8 @@ export async function cmd({ config }: { config: AstroConfig; flags: Arguments })
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrationQueries = await getMigrationQueries({
|
const migrationQueries = await getMigrationQueries({
|
||||||
oldCollections: prevSnapshot,
|
oldSnapshot: prevSnapshot,
|
||||||
newCollections: currentSnapshot,
|
newSnapshot: currentSnapshot,
|
||||||
});
|
});
|
||||||
|
|
||||||
const largestNumber = allMigrationFiles.reduce((acc, curr) => {
|
const largestNumber = allMigrationFiles.reduce((acc, curr) => {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { AstroConfig } from 'astro';
|
||||||
import deepDiff from 'deep-diff';
|
import deepDiff from 'deep-diff';
|
||||||
import type { Arguments } from 'yargs-parser';
|
import type { Arguments } from 'yargs-parser';
|
||||||
import { getMigrations, initializeFromMigrations } from '../../../migrations.js';
|
import { getMigrations, initializeFromMigrations } from '../../../migrations.js';
|
||||||
const { diff, applyChange } = deepDiff;
|
const { diff } = deepDiff;
|
||||||
|
|
||||||
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
|
export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
|
||||||
const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
|
const currentSnapshot = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
||||||
DBCollections,
|
DBCollections,
|
||||||
DBField,
|
DBField,
|
||||||
DBFields,
|
DBFields,
|
||||||
|
DBSnapshot,
|
||||||
DateField,
|
DateField,
|
||||||
FieldType,
|
FieldType,
|
||||||
JsonField,
|
JsonField,
|
||||||
|
@ -25,18 +26,17 @@ interface PromptResponses {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMigrationQueries({
|
export async function getMigrationQueries({
|
||||||
oldCollections,
|
oldSnapshot,
|
||||||
newCollections,
|
newSnapshot,
|
||||||
promptResponses,
|
promptResponses,
|
||||||
}: {
|
}: {
|
||||||
oldCollections: DBCollections;
|
oldSnapshot: DBSnapshot;
|
||||||
newCollections: DBCollections;
|
newSnapshot: DBSnapshot;
|
||||||
promptResponses?: PromptResponses;
|
promptResponses?: PromptResponses;
|
||||||
}) {
|
}): Promise<string[]> {
|
||||||
const queries: string[] = [];
|
const queries: string[] = [];
|
||||||
let added = getAddedCollections(oldCollections, newCollections);
|
let added = getAddedCollections(oldSnapshot, newSnapshot);
|
||||||
let dropped = getDroppedCollections(oldCollections, newCollections);
|
let dropped = getDroppedCollections(oldSnapshot, newSnapshot);
|
||||||
|
|
||||||
if (!isEmpty(added) && !isEmpty(dropped)) {
|
if (!isEmpty(added) && !isEmpty(dropped)) {
|
||||||
const resolved = await resolveCollectionRenames(
|
const resolved = await resolveCollectionRenames(
|
||||||
added,
|
added,
|
||||||
|
@ -62,8 +62,8 @@ export async function getMigrationQueries({
|
||||||
queries.push(dropQuery);
|
queries.push(dropQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [collectionName, newCollection] of Object.entries(newCollections)) {
|
for (const [collectionName, newCollection] of Object.entries(newSnapshot.schema)) {
|
||||||
const oldCollection = oldCollections[collectionName];
|
const oldCollection = oldSnapshot.schema[collectionName];
|
||||||
if (!oldCollection) continue;
|
if (!oldCollection) continue;
|
||||||
const collectionChangeQueries = await getCollectionChangeQueries({
|
const collectionChangeQueries = await getCollectionChangeQueries({
|
||||||
collectionName,
|
collectionName,
|
||||||
|
@ -130,12 +130,9 @@ export async function getCollectionChangeQueries({
|
||||||
)} field ${color.blue(color.bold(fieldName))}. ${color.red(
|
)} field ${color.blue(color.bold(fieldName))}. ${color.red(
|
||||||
'This will delete all existing data in the collection!'
|
'This will delete all existing data in the collection!'
|
||||||
)} We recommend setting a default value to avoid data loss.`,
|
)} We recommend setting a default value to avoid data loss.`,
|
||||||
'updated-required': `Changing ${color.blue(color.bold(collectionName))} field ${color.blue(
|
'added-unique': `Adding unique ${color.blue(color.bold(collectionName))} field ${color.blue(
|
||||||
color.bold(fieldName)
|
color.bold(fieldName)
|
||||||
)} to required. ${color.red('This will delete all existing data in the collection!')}`,
|
)}. ${color.red('This will delete all existing data in the collection!')}`,
|
||||||
'updated-unique': `Changing ${color.blue(color.bold(collectionName))} field ${color.blue(
|
|
||||||
color.bold(fieldName)
|
|
||||||
)} to unique. ${color.red('This will delete all existing data in the collection!')}`,
|
|
||||||
'updated-type': `Changing the type of ${color.blue(
|
'updated-type': `Changing the type of ${color.blue(
|
||||||
color.bold(collectionName)
|
color.bold(collectionName)
|
||||||
)} field ${color.blue(color.bold(fieldName))}. ${color.red(
|
)} field ${color.blue(color.bold(fieldName))}. ${color.red(
|
||||||
|
@ -159,11 +156,22 @@ export async function getCollectionChangeQueries({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addedPrimaryKey = Object.entries(added).find(
|
||||||
|
([, field]) => hasPrimaryKey(field)
|
||||||
|
);
|
||||||
|
const droppedPrimaryKey = Object.entries(dropped).find(
|
||||||
|
([, field]) => hasPrimaryKey(field)
|
||||||
|
);
|
||||||
|
const updatedPrimaryKey = Object.entries(updated).find(
|
||||||
|
([, field]) =>
|
||||||
|
(hasPrimaryKey(field.old) || hasPrimaryKey(field.new))
|
||||||
|
);
|
||||||
const recreateTableQueries = getRecreateTableQueries({
|
const recreateTableQueries = getRecreateTableQueries({
|
||||||
unescCollectionName: collectionName,
|
unescapedCollectionName: collectionName,
|
||||||
newCollection,
|
newCollection,
|
||||||
added,
|
added,
|
||||||
hasDataLoss: dataLossCheck.dataLoss,
|
hasDataLoss: dataLossCheck.dataLoss,
|
||||||
|
migrateHiddenPrimaryKey: !addedPrimaryKey && !droppedPrimaryKey && !updatedPrimaryKey,
|
||||||
});
|
});
|
||||||
queries.push(...recreateTableQueries);
|
queries.push(...recreateTableQueries);
|
||||||
return queries;
|
return queries;
|
||||||
|
@ -288,25 +296,31 @@ async function resolveCollectionRenames(
|
||||||
return { added, dropped, renamed };
|
return { added, dropped, renamed };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAddedCollections(oldCollections: DBCollections, newCollections: DBCollections) {
|
function getAddedCollections(
|
||||||
|
oldCollections: DBSnapshot,
|
||||||
|
newCollections: DBSnapshot
|
||||||
|
): DBCollections {
|
||||||
const added: DBCollections = {};
|
const added: DBCollections = {};
|
||||||
for (const [key, newCollection] of Object.entries(newCollections)) {
|
for (const [key, newCollection] of Object.entries(newCollections.schema)) {
|
||||||
if (!(key in oldCollections)) added[key] = newCollection;
|
if (!(key in oldCollections.schema)) added[key] = newCollection;
|
||||||
}
|
}
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDroppedCollections(oldCollections: DBCollections, newCollections: DBCollections) {
|
function getDroppedCollections(
|
||||||
|
oldCollections: DBSnapshot,
|
||||||
|
newCollections: DBSnapshot
|
||||||
|
): DBCollections {
|
||||||
const dropped: DBCollections = {};
|
const dropped: DBCollections = {};
|
||||||
for (const [key, oldCollection] of Object.entries(oldCollections)) {
|
for (const [key, oldCollection] of Object.entries(oldCollections.schema)) {
|
||||||
if (!(key in newCollections)) dropped[key] = oldCollection;
|
if (!(key in newCollections.schema)) dropped[key] = oldCollection;
|
||||||
}
|
}
|
||||||
return dropped;
|
return dropped;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFieldRenameQueries(unescCollectionName: string, renamed: Renamed): string[] {
|
function getFieldRenameQueries(unescapedCollectionName: string, renamed: Renamed): string[] {
|
||||||
const queries: string[] = [];
|
const queries: string[] = [];
|
||||||
const collectionName = sqlite.escapeName(unescCollectionName);
|
const collectionName = sqlite.escapeName(unescapedCollectionName);
|
||||||
|
|
||||||
for (const { from, to } of renamed) {
|
for (const { from, to } of renamed) {
|
||||||
const q = `ALTER TABLE ${collectionName} RENAME COLUMN ${sqlite.escapeName(
|
const q = `ALTER TABLE ${collectionName} RENAME COLUMN ${sqlite.escapeName(
|
||||||
|
@ -323,12 +337,12 @@ function getFieldRenameQueries(unescCollectionName: string, renamed: Renamed): s
|
||||||
* `canUseAlterTableAddColumn` and `canAlterTableDropColumn` checks!
|
* `canUseAlterTableAddColumn` and `canAlterTableDropColumn` checks!
|
||||||
*/
|
*/
|
||||||
function getAlterTableQueries(
|
function getAlterTableQueries(
|
||||||
unescCollectionName: string,
|
unescapedCollectionName: string,
|
||||||
added: DBFields,
|
added: DBFields,
|
||||||
dropped: DBFields
|
dropped: DBFields
|
||||||
): string[] {
|
): string[] {
|
||||||
const queries: string[] = [];
|
const queries: string[] = [];
|
||||||
const collectionName = sqlite.escapeName(unescCollectionName);
|
const collectionName = sqlite.escapeName(unescapedCollectionName);
|
||||||
|
|
||||||
for (const [unescFieldName, field] of Object.entries(added)) {
|
for (const [unescFieldName, field] of Object.entries(added)) {
|
||||||
const fieldName = sqlite.escapeName(unescFieldName);
|
const fieldName = sqlite.escapeName(unescFieldName);
|
||||||
|
@ -350,40 +364,52 @@ function getAlterTableQueries(
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRecreateTableQueries({
|
function getRecreateTableQueries({
|
||||||
unescCollectionName,
|
unescapedCollectionName,
|
||||||
newCollection,
|
newCollection,
|
||||||
added,
|
added,
|
||||||
hasDataLoss,
|
hasDataLoss,
|
||||||
|
migrateHiddenPrimaryKey,
|
||||||
}: {
|
}: {
|
||||||
unescCollectionName: string;
|
unescapedCollectionName: string;
|
||||||
newCollection: DBCollection;
|
newCollection: DBCollection;
|
||||||
added: Record<string, DBField>;
|
added: Record<string, DBField>;
|
||||||
hasDataLoss: boolean;
|
hasDataLoss: boolean;
|
||||||
|
migrateHiddenPrimaryKey: boolean;
|
||||||
}): string[] {
|
}): string[] {
|
||||||
const unescTempName = `${unescCollectionName}_${genTempTableName()}`;
|
const unescTempName = `${unescapedCollectionName}_${genTempTableName()}`;
|
||||||
const tempName = sqlite.escapeName(unescTempName);
|
const tempName = sqlite.escapeName(unescTempName);
|
||||||
const collectionName = sqlite.escapeName(unescCollectionName);
|
const collectionName = sqlite.escapeName(unescapedCollectionName);
|
||||||
const queries = [getCreateTableQuery(unescTempName, newCollection)];
|
|
||||||
|
|
||||||
if (!hasDataLoss) {
|
if (hasDataLoss) {
|
||||||
const newColumns = ['id', ...Object.keys(newCollection.fields)];
|
return [`DROP TABLE ${collectionName}`, getCreateTableQuery(collectionName, newCollection)];
|
||||||
const originalColumns = newColumns.filter((i) => !(i in added));
|
|
||||||
const escapedColumns = originalColumns.map((c) => sqlite.escapeName(c)).join(', ');
|
|
||||||
|
|
||||||
queries.push(
|
|
||||||
`INSERT INTO ${tempName} (${escapedColumns}) SELECT ${escapedColumns} FROM ${collectionName}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
const newColumns = [...Object.keys(newCollection.fields)];
|
||||||
|
if (migrateHiddenPrimaryKey) {
|
||||||
|
newColumns.unshift('_id');
|
||||||
|
}
|
||||||
|
const escapedColumns = newColumns
|
||||||
|
.filter((i) => !(i in added))
|
||||||
|
.map((c) => sqlite.escapeName(c))
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
queries.push(`DROP TABLE ${collectionName}`);
|
return [
|
||||||
queries.push(`ALTER TABLE ${tempName} RENAME TO ${collectionName}`);
|
getCreateTableQuery(unescTempName, newCollection),
|
||||||
return queries;
|
`INSERT INTO ${tempName} (${escapedColumns}) SELECT ${escapedColumns} FROM ${collectionName}`,
|
||||||
|
`DROP TABLE ${collectionName}`,
|
||||||
|
`ALTER TABLE ${tempName} RENAME TO ${collectionName}`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCreateTableQuery(collectionName: string, collection: DBCollection) {
|
export function getCreateTableQuery(collectionName: string, collection: DBCollection) {
|
||||||
let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
|
let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
|
||||||
|
|
||||||
const colQueries = ['"id" text PRIMARY KEY'];
|
const colQueries = [];
|
||||||
|
const colHasPrimaryKey = Object.entries(collection.fields).find(
|
||||||
|
([, field]) => hasPrimaryKey(field)
|
||||||
|
);
|
||||||
|
if (!colHasPrimaryKey) {
|
||||||
|
colQueries.push('_id INTEGER PRIMARY KEY AUTOINCREMENT');
|
||||||
|
}
|
||||||
for (const [columnName, column] of Object.entries(collection.fields)) {
|
for (const [columnName, column] of Object.entries(collection.fields)) {
|
||||||
const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
|
const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
|
||||||
column.type
|
column.type
|
||||||
|
@ -395,16 +421,19 @@ export function getCreateTableQuery(collectionName: string, collection: DBCollec
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModifiers(columnName: string, column: DBField) {
|
function getModifiers(fieldName: string, field: DBField) {
|
||||||
let modifiers = '';
|
let modifiers = '';
|
||||||
if (!column.optional) {
|
if (hasPrimaryKey(field)) {
|
||||||
|
modifiers += ' PRIMARY KEY';
|
||||||
|
}
|
||||||
|
if (!field.optional) {
|
||||||
modifiers += ' NOT NULL';
|
modifiers += ' NOT NULL';
|
||||||
}
|
}
|
||||||
if (column.unique) {
|
if (field.unique) {
|
||||||
modifiers += ' UNIQUE';
|
modifiers += ' UNIQUE';
|
||||||
}
|
}
|
||||||
if (hasDefault(column)) {
|
if (hasDefault(field)) {
|
||||||
modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`;
|
modifiers += ` DEFAULT ${getDefaultValueSql(fieldName, field)}`;
|
||||||
}
|
}
|
||||||
return modifiers;
|
return modifiers;
|
||||||
}
|
}
|
||||||
|
@ -431,19 +460,24 @@ function isEmpty(obj: Record<string, unknown>) {
|
||||||
*
|
*
|
||||||
* @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column
|
* @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column
|
||||||
*/
|
*/
|
||||||
function canAlterTableAddColumn(column: DBField) {
|
function canAlterTableAddColumn(field: DBField) {
|
||||||
if (column.unique) return false;
|
if (field.unique) return false;
|
||||||
if (hasRuntimeDefault(column)) return false;
|
if (hasRuntimeDefault(field)) return false;
|
||||||
if (!column.optional && !hasDefault(column)) return false;
|
if (!field.optional && !hasDefault(field)) return false;
|
||||||
|
if (hasPrimaryKey(field)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canAlterTableDropColumn(column: DBField) {
|
function canAlterTableDropColumn(field: DBField) {
|
||||||
if (column.unique) return false;
|
if (field.unique) return false;
|
||||||
|
if (hasPrimaryKey(field)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataLossReason = 'added-required' | 'updated-required' | 'updated-unique' | 'updated-type';
|
type DataLossReason =
|
||||||
|
| 'added-required'
|
||||||
|
| 'added-unique'
|
||||||
|
| 'updated-type';
|
||||||
type DataLossResponse =
|
type DataLossResponse =
|
||||||
| { dataLoss: false }
|
| { dataLoss: false }
|
||||||
| { dataLoss: true; fieldName: string; reason: DataLossReason };
|
| { dataLoss: true; fieldName: string; reason: DataLossReason };
|
||||||
|
@ -453,19 +487,20 @@ function canRecreateTableWithoutDataLoss(
|
||||||
updated: UpdatedFields
|
updated: UpdatedFields
|
||||||
): DataLossResponse {
|
): DataLossResponse {
|
||||||
for (const [fieldName, a] of Object.entries(added)) {
|
for (const [fieldName, a] of Object.entries(added)) {
|
||||||
if (!a.optional && !hasDefault(a))
|
if (hasPrimaryKey(a) && a.type !== 'number') {
|
||||||
return { dataLoss: true, fieldName, reason: 'added-required' };
|
return { dataLoss: true, fieldName, reason: 'added-required' };
|
||||||
|
}
|
||||||
|
if (!a.optional && !hasDefault(a)) {
|
||||||
|
return { dataLoss: true, fieldName, reason: 'added-required' };
|
||||||
|
}
|
||||||
|
if (a.unique) {
|
||||||
|
return { dataLoss: true, fieldName, reason: 'added-unique' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (const [fieldName, u] of Object.entries(updated)) {
|
for (const [fieldName, u] of Object.entries(updated)) {
|
||||||
// Cannot respect default value when changing to NOT NULL
|
if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new)) {
|
||||||
// TODO: use UPDATE query to set default where IS NULL
|
|
||||||
// @see https://dataschool.com/learn-sql/how-to-replace-nulls-with-0s/
|
|
||||||
if (u.old.optional && !u.new.optional)
|
|
||||||
return { dataLoss: true, fieldName, reason: 'updated-required' };
|
|
||||||
if (!u.old.unique && u.new.unique)
|
|
||||||
return { dataLoss: true, fieldName, reason: 'updated-unique' };
|
|
||||||
if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new))
|
|
||||||
return { dataLoss: true, fieldName, reason: 'updated-type' };
|
return { dataLoss: true, fieldName, reason: 'updated-type' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { dataLoss: false };
|
return { dataLoss: false };
|
||||||
}
|
}
|
||||||
|
@ -535,9 +570,19 @@ type DBFieldWithDefault =
|
||||||
| WithDefaultDefined<BooleanField>
|
| WithDefaultDefined<BooleanField>
|
||||||
| WithDefaultDefined<JsonField>;
|
| WithDefaultDefined<JsonField>;
|
||||||
|
|
||||||
|
function hasPrimaryKey(field: DBField) {
|
||||||
|
return 'primaryKey' in field && !!field.primaryKey;
|
||||||
|
}
|
||||||
|
|
||||||
// Type narrowing the default fails on union types, so use a type guard
|
// Type narrowing the default fails on union types, so use a type guard
|
||||||
function hasDefault(field: DBField): field is DBFieldWithDefault {
|
function hasDefault(field: DBField): field is DBFieldWithDefault {
|
||||||
return field.default !== undefined;
|
if (field.default !== undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (hasPrimaryKey(field) && field.type === 'number') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault {
|
function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault {
|
||||||
|
@ -549,7 +594,7 @@ function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): str
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return column.default ? 'TRUE' : 'FALSE';
|
return column.default ? 'TRUE' : 'FALSE';
|
||||||
case 'number':
|
case 'number':
|
||||||
return `${column.default}`;
|
return `${column.default || 'AUTOINCREMENT'}`;
|
||||||
case 'text':
|
case 'text':
|
||||||
return sqlite.escapeString(column.default);
|
return sqlite.escapeString(column.default);
|
||||||
case 'date':
|
case 'date':
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
// import type { DBCollections } from '../../types.js';
|
|
||||||
// import yargs from 'yargs-parser';
|
|
||||||
// import { appTokenError } from '../../errors.js';
|
|
||||||
// import {
|
|
||||||
// getAstroStudioEnv,
|
|
||||||
// getStudioUrl,
|
|
||||||
// getRemoteDatabaseUrl,
|
|
||||||
// isAppTokenValid,
|
|
||||||
// } from '../../utils.js';
|
|
||||||
// import { migrate } from './migrate.js';
|
|
||||||
// import { collectionToTable, createLocalDatabaseClient } from '../../internal.js';
|
|
||||||
// import type { Query } from 'drizzle-orm';
|
|
||||||
// import type { InArgs, InStatement } from '@libsql/client';
|
|
||||||
|
|
||||||
// export async function sync({ collections }: { collections: DBCollections }) {
|
|
||||||
// const args = yargs(process.argv.slice(3), {
|
|
||||||
// string: ['dry-run', 'seed'],
|
|
||||||
// });
|
|
||||||
// const isDryRun = 'dry-run' in args;
|
|
||||||
// const shouldSeed = 'seed' in args;
|
|
||||||
// const remoteDbUrl = getRemoteDatabaseUrl();
|
|
||||||
|
|
||||||
// const appToken = getAstroStudioEnv().ASTRO_STUDIO_APP_TOKEN;
|
|
||||||
// if (!appToken || !(await isAppTokenValid({ remoteDbUrl, appToken }))) {
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.error(appTokenError);
|
|
||||||
// process.exit(1);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// await setSyncStatus({ status: 'RUNNING', remoteDbUrl, appToken });
|
|
||||||
// await migrate({ collections, isDryRun, appToken });
|
|
||||||
// await setSyncStatus({ status: 'SUCCESS', remoteDbUrl, appToken });
|
|
||||||
// if (shouldSeed) {
|
|
||||||
// await tempDataPush({ collections, appToken, isDryRun });
|
|
||||||
// }
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.info('Sync complete 🔄');
|
|
||||||
// } catch (e) {
|
|
||||||
// await setSyncStatus({ status: 'FAILED', remoteDbUrl, appToken });
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.error(e);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /** TODO: refine with migration changes */
|
|
||||||
// async function tempDataPush({
|
|
||||||
// collections,
|
|
||||||
// appToken,
|
|
||||||
// isDryRun,
|
|
||||||
// }: {
|
|
||||||
// collections: DBCollections;
|
|
||||||
// appToken: string;
|
|
||||||
// isDryRun?: boolean;
|
|
||||||
// }) {
|
|
||||||
// const db = await createLocalDatabaseClient({ collections, dbUrl: ':memory:', seeding: true });
|
|
||||||
// const queries: Query[] = [];
|
|
||||||
|
|
||||||
// for (const [name, collection] of Object.entries(collections)) {
|
|
||||||
// if (collection.writable || !collection.data) continue;
|
|
||||||
// const table = collectionToTable(name, collection);
|
|
||||||
// const insert = db.insert(table).values(await collection.data());
|
|
||||||
|
|
||||||
// queries.push(insert.toSQL());
|
|
||||||
// }
|
|
||||||
// const url = new URL('/db/query', getRemoteDatabaseUrl());
|
|
||||||
// const requestBody: InStatement[] = queries.map((q) => ({
|
|
||||||
// sql: q.sql,
|
|
||||||
// args: q.params as InArgs,
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// if (isDryRun) {
|
|
||||||
// console.info('[DRY RUN] Batch data seed:', JSON.stringify(requestBody, null, 2));
|
|
||||||
// return new Response(null, { status: 200 });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return await fetch(url, {
|
|
||||||
// method: 'POST',
|
|
||||||
// headers: new Headers({
|
|
||||||
// Authorization: `Bearer ${appToken}`,
|
|
||||||
// }),
|
|
||||||
// body: JSON.stringify(requestBody),
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async function setSyncStatus({
|
|
||||||
// remoteDbUrl,
|
|
||||||
// appToken,
|
|
||||||
// status,
|
|
||||||
// }: {
|
|
||||||
// remoteDbUrl: string;
|
|
||||||
// appToken: string;
|
|
||||||
// status: 'RUNNING' | 'FAILED' | 'SUCCESS';
|
|
||||||
// }) {
|
|
||||||
// const syncStatusUrl = new URL('/api/rest/sync-status', getStudioUrl());
|
|
||||||
// syncStatusUrl.searchParams.set('workerUrl', remoteDbUrl);
|
|
||||||
// syncStatusUrl.searchParams.set('status', status);
|
|
||||||
|
|
||||||
// const response = await fetch(syncStatusUrl, {
|
|
||||||
// method: 'PATCH',
|
|
||||||
// headers: {
|
|
||||||
// Authorization: `Bearer ${appToken}`,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (!response.ok) {
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.error('Unexpected problem completing sync.');
|
|
||||||
// process.exit(1);
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1,105 +0,0 @@
|
||||||
// import type { InArgs, InStatement } from '@libsql/client';
|
|
||||||
// import {
|
|
||||||
// STUDIO_ADMIN_TABLE_ROW_ID,
|
|
||||||
// adminTable,
|
|
||||||
// } from './admin.js';
|
|
||||||
// import { type SQL, eq, sql } from 'drizzle-orm';
|
|
||||||
// import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
|
|
||||||
// import { red } from 'kleur/colors';
|
|
||||||
// import { collectionsSchema, type DBCollections } from '../../types.js';
|
|
||||||
// import { getRemoteDatabaseUrl } from '../../utils.js';
|
|
||||||
// import { authenticationError, unexpectedAstroAdminError } from '../../errors.js';
|
|
||||||
// import { createLocalDatabaseClient } from './remote-db.js';
|
|
||||||
// import { getMigrationQueries } from './queries.js';
|
|
||||||
|
|
||||||
// const sqliteDialect = new SQLiteAsyncDialect();
|
|
||||||
|
|
||||||
// export async function migrate({
|
|
||||||
// collections: newCollections,
|
|
||||||
// appToken,
|
|
||||||
// isDryRun,
|
|
||||||
// }: {
|
|
||||||
// collections: DBCollections;
|
|
||||||
// appToken: string;
|
|
||||||
// isDryRun?: boolean;
|
|
||||||
// }) {
|
|
||||||
// const db = createLocalDatabaseClient(appToken);
|
|
||||||
// const adminEntry = await db
|
|
||||||
// .select({ collections: adminTable.collections })
|
|
||||||
// .from(adminTable)
|
|
||||||
// .get();
|
|
||||||
|
|
||||||
// if (!adminEntry) {
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.error(
|
|
||||||
// `${red(
|
|
||||||
// '⚠️ Unexpected error syncing collections.',
|
|
||||||
// )} You may need to initialize a new project with \`studio init\`.`,
|
|
||||||
// );
|
|
||||||
// process.exit(1);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (JSON.stringify(newCollections) === adminEntry.collections) {
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.info('Collections already up to date');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const oldCollections = collectionsSchema.parse(
|
|
||||||
// adminEntry.collections ? JSON.parse(adminEntry.collections) : {},
|
|
||||||
// );
|
|
||||||
// const queries: SQL[] = [];
|
|
||||||
// const migrationQueries = await getMigrationQueries({ oldCollections, newCollections });
|
|
||||||
// queries.push(...migrationQueries.map((q) => sql.raw(q)));
|
|
||||||
|
|
||||||
// const updateCollectionsJson = db
|
|
||||||
// .update(adminTable)
|
|
||||||
// .set({ collections: JSON.stringify(newCollections) })
|
|
||||||
// .where(eq(adminTable.id, STUDIO_ADMIN_TABLE_ROW_ID))
|
|
||||||
// .getSQL();
|
|
||||||
// queries.push(updateCollectionsJson);
|
|
||||||
|
|
||||||
// const res = await runBatchQuery({ queries, appToken, isDryRun });
|
|
||||||
// if (!res.ok) {
|
|
||||||
// if (res.status === 401) {
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.error(authenticationError);
|
|
||||||
// process.exit(1);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // eslint-disable-next-line no-console
|
|
||||||
// console.error(unexpectedAstroAdminError);
|
|
||||||
// process.exit(1);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// async function runBatchQuery({
|
|
||||||
// queries: sqlQueries,
|
|
||||||
// appToken,
|
|
||||||
// isDryRun,
|
|
||||||
// }: {
|
|
||||||
// queries: SQL[];
|
|
||||||
// appToken: string;
|
|
||||||
// isDryRun?: boolean;
|
|
||||||
// }) {
|
|
||||||
// const queries = sqlQueries.map((q) => sqliteDialect.sqlToQuery(q));
|
|
||||||
// const requestBody: InStatement[] = queries.map((q) => ({
|
|
||||||
// sql: q.sql,
|
|
||||||
// args: q.params as InArgs,
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// if (isDryRun) {
|
|
||||||
// console.info('[DRY RUN] Batch query:', JSON.stringify(requestBody, null, 2));
|
|
||||||
// return new Response(null, { status: 200 });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const url = new URL('/db/query', getRemoteDatabaseUrl());
|
|
||||||
|
|
||||||
// return await fetch(url, {
|
|
||||||
// method: 'POST',
|
|
||||||
// headers: new Headers({
|
|
||||||
// Authorization: `Bearer ${appToken}`,
|
|
||||||
// }),
|
|
||||||
// body: JSON.stringify(requestBody),
|
|
||||||
// });
|
|
||||||
// }
|
|
|
@ -1,6 +1,7 @@
|
||||||
import deepDiff from 'deep-diff';
|
import deepDiff from 'deep-diff';
|
||||||
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
|
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
|
||||||
import type { DBCollections } from './types.js';
|
import type { DBSnapshot } from './types.js';
|
||||||
|
import type { AstroConfig } from 'astro';
|
||||||
const { applyChange } = deepDiff;
|
const { applyChange } = deepDiff;
|
||||||
|
|
||||||
export async function getMigrations(): Promise<string[]> {
|
export async function getMigrations(): Promise<string[]> {
|
||||||
|
@ -17,18 +18,20 @@ export async function loadMigration(migration: string): Promise<{ diff: any[]; d
|
||||||
return JSON.parse(await readFile(`./migrations/${migration}`, 'utf-8'));
|
return JSON.parse(await readFile(`./migrations/${migration}`, 'utf-8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadInitialSnapshot(): Promise<DBCollections> {
|
export async function loadInitialSnapshot(): Promise<DBSnapshot> {
|
||||||
return JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8'));
|
const snapshot = JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8'));
|
||||||
|
if (!snapshot.version) {
|
||||||
|
return { version: 2, meta: {}, schema: snapshot };
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeMigrationsDirectory(currentSnapshot: unknown) {
|
export async function initializeMigrationsDirectory(currentSnapshot: DBSnapshot) {
|
||||||
await mkdir('./migrations', { recursive: true });
|
await mkdir('./migrations', { recursive: true });
|
||||||
await writeFile('./migrations/0000_snapshot.json', JSON.stringify(currentSnapshot, undefined, 2));
|
await writeFile('./migrations/0000_snapshot.json', JSON.stringify(currentSnapshot, undefined, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeFromMigrations(
|
export async function initializeFromMigrations(allMigrationFiles: string[]): Promise<DBSnapshot> {
|
||||||
allMigrationFiles: string[]
|
|
||||||
): Promise<DBCollections> {
|
|
||||||
const prevSnapshot = await loadInitialSnapshot();
|
const prevSnapshot = await loadInitialSnapshot();
|
||||||
for (const migration of allMigrationFiles) {
|
for (const migration of allMigrationFiles) {
|
||||||
if (migration === '0000_snapshot.json') continue;
|
if (migration === '0000_snapshot.json') continue;
|
||||||
|
@ -39,3 +42,11 @@ export async function initializeFromMigrations(
|
||||||
}
|
}
|
||||||
return prevSnapshot;
|
return prevSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCurrentSnapshot(config: AstroConfig): DBSnapshot {
|
||||||
|
const schema = JSON.parse(JSON.stringify(config.db?.collections ?? {}));
|
||||||
|
return { version: 2, meta: {}, schema };
|
||||||
|
}
|
||||||
|
export function createEmptySnapshot(): DBSnapshot {
|
||||||
|
return { version: 2, meta: {}, schema: {} };
|
||||||
|
}
|
||||||
|
|
|
@ -16,12 +16,14 @@ const booleanFieldSchema = baseFieldSchema.extend({
|
||||||
const numberFieldSchema = baseFieldSchema.extend({
|
const numberFieldSchema = baseFieldSchema.extend({
|
||||||
type: z.literal('number'),
|
type: z.literal('number'),
|
||||||
default: z.number().optional(),
|
default: z.number().optional(),
|
||||||
|
primaryKey: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const textFieldSchema = baseFieldSchema.extend({
|
const textFieldSchema = baseFieldSchema.extend({
|
||||||
type: z.literal('text'),
|
type: z.literal('text'),
|
||||||
multiline: z.boolean().optional(),
|
multiline: z.boolean().optional(),
|
||||||
default: z.string().optional(),
|
default: z.string().optional(),
|
||||||
|
primaryKey: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const dateFieldSchema = baseFieldSchema.extend({
|
const dateFieldSchema = baseFieldSchema.extend({
|
||||||
|
@ -95,6 +97,11 @@ export type DBCollection = z.infer<
|
||||||
typeof readableCollectionSchema | typeof writableCollectionSchema
|
typeof readableCollectionSchema | typeof writableCollectionSchema
|
||||||
>;
|
>;
|
||||||
export type DBCollections = Record<string, DBCollection>;
|
export type DBCollections = Record<string, DBCollection>;
|
||||||
|
export type DBSnapshot = {
|
||||||
|
version: number;
|
||||||
|
meta: Record<string, never>;
|
||||||
|
schema: Record<string, DBCollection>;
|
||||||
|
};
|
||||||
export type ReadableDBCollection = z.infer<typeof readableCollectionSchema>;
|
export type ReadableDBCollection = z.infer<typeof readableCollectionSchema>;
|
||||||
export type WritableDBCollection = z.infer<typeof writableCollectionSchema>;
|
export type WritableDBCollection = z.infer<typeof writableCollectionSchema>;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue