Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/backend/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { stubsRoot } from './stubs/main.js';
import { fsReadAll } from '@poppinss/utils';

async function addMigrations(command: Configure, codemods: Codemods) {
const allMigrations = ['base', 'audit', 'drops', 'preferences', 'campaigns'];
const allMigrations = ['base', 'audit', 'drops', 'preferences', 'campaigns', 'configs'];

const path = command.app.migrationsPath();
const migrations = await fsReadAll(path);
Expand Down Expand Up @@ -102,6 +102,7 @@ export async function configure(command: Configure) {
await codemods.makeUsingStub(stubsRoot, 'resources/views/preview.stub', {});
await codemods.makeUsingStub(stubsRoot, 'resources/views/scripture.stub', {});

await codemods.makeUsingStub(stubsRoot, 'commands/migrate_config.stub', {});
await codemods.makeUsingStub(stubsRoot, 'commands/make_user.stub', {});
await codemods.makeUsingStub(stubsRoot, 'commands/fix_database.stub', {});
// NOTE: remove when this is resolved: https://github.com/adonisjs/inertia-starter-kit/issues/12
Expand Down
1 change: 1 addition & 0 deletions src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { default as Index } from './models/index.js';
export { default as Drop } from './models/drop.js';
export { default as User } from './models/user.js';
export { default as Preference } from './models/preference.js';
export { default as Config } from './models/config.js';
export * from './factories/drop_factory.js';
export * from './factories/draft_factory.js';
export * from './factories/index_factory.js';
Expand Down
22 changes: 22 additions & 0 deletions src/backend/models/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { DateTime } from 'luxon';
import { BaseModel, column } from '@adonisjs/lucid/orm';

export default class Config extends BaseModel {
@column({ isPrimary: true })
declare id: number;

@column()
declare key: string;

@column()
declare version: number;

@column()
declare data: Record<string, any>;

@column.dateTime({ autoCreate: true })
declare createdAt: DateTime;

@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime;
}
27 changes: 22 additions & 5 deletions src/backend/providers/cms_provider.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import { CmsService } from '../services/cms_service.js';
import type { ApplicationService } from '@adonisjs/core/types';
import Config from '../models/config.js';
import { CmsConfig } from '../../types.js';

export default class CmsProvider {
constructor(protected app: ApplicationService) {}

register() {
this.app.container.singleton(CmsService, async (resolver) => {
// currently reading from config file
// NEXT: read from database
const configService = await resolver.make('config');
const cmsConfig = configService.get<any>('cms');
// check to see if we have a config in the database
const persisted = await Config.query()
.where('key', 'cms')
.orderBy('version', 'desc')
.first();

return new CmsService(cmsConfig);
let cmsConfig: CmsConfig;
if (persisted) {
cmsConfig = persisted.data as CmsConfig;
} else {
// save the default config to the database
const configService = await resolver.make('config');
cmsConfig = configService.get<any>('cms') as CmsConfig;
await Config.create({
key: 'cms',
version: 1,
data: cmsConfig as Record<string, any>,
});
}

return new CmsService(cmsConfig!);
});
}
}
4 changes: 4 additions & 0 deletions src/backend/services/cms_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export class CmsService {
return this.#config;
}

public set config(config: CmsConfig) {
this.#config = config;
}

public storyFrom(ctx: HttpContext): StorySpec | undefined {
const storyId = Number.parseInt(ctx.params.storyId);
const story = this.#config.stories.stories.find((s) => s.id === storyId);
Expand Down
56 changes: 56 additions & 0 deletions src/backend/stubs/commands/migrate_config.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{{{
exports({ to: app.commandsPath('migrate_config.ts') })
}}}
import { BaseCommand } from '@adonisjs/core/ace';
import type { CommandOptions } from '@adonisjs/core/types/ace';
import { CmsConfig, Config, defineConfig } from '@story-cms/kit';

export default class MigrateConfig extends BaseCommand {
static commandName = 'migrate:config';
static description = 'Migrate the active configuration';

static options: CommandOptions = { startApp: true };

async run() {
// fetch active configuration
const active = await Config.query()
.where('key', 'cms')
.orderBy('version', 'desc')
.first();

if (!active) {
this.logger.info('No active configuration found, skipping migration');
return;
}

try {
const overrides = active.data as CmsConfig;
const newConfig = defineConfig(overrides);
if (this.isEqual(newConfig, overrides)) {
this.logger.info('Configuration is up to date, skipping migration');
return;
}

const config = new Config();
config.key = 'cms';
config.version = active.version + 1;
config.data = newConfig;
await config.save();
this.logger.info('Configuration migrated successfully');
} catch (error) {
this.logger.error('Error migrating configuration: ' + error);
}
}

private isEqual(a: CmsConfig, b: CmsConfig): boolean {
const sortedStringify = (obj: unknown): string =>
JSON.stringify(obj, (_key, value) =>
value && typeof value === 'object' && !Array.isArray(value)
? Object.fromEntries(
Object.entries(value).sort(([x], [y]) => x.localeCompare(y)),
)
: value,
);
return sortedStringify(a) === sortedStringify(b);
}
}
29 changes: 29 additions & 0 deletions src/backend/stubs/migrations/configs.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{{{
exports({
to: app.makePath(migration.folder, migration.fileName)
})
}}}
import { BaseSchema } from '@adonisjs/lucid/schema';

export default class extends BaseSchema {
protected tableName = 'configs';

async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id');

table.string('key').notNullable();
table.integer('version').notNullable().defaultTo(1);
table.jsonb('data').notNullable().defaultTo('{}');

table.timestamp('created_at');
table.timestamp('updated_at');

table.unique(['key', 'version']);
});
}

async down() {
this.schema.dropTable(this.tableName);
}
}
73 changes: 73 additions & 0 deletions src/backend/stubs/tests/unit/config.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{{{
exports({ to: app.makePath('tests/unit/configs.spec.ts') })
}}}
import { test } from '@japa/runner';
import testUtils from '@adonisjs/core/services/test_utils';
import { Config } from '@story-cms/kit';
import cmsConfig from '#config/cms';

test.group('Config', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction());

test('can store cms config in configs table', async ({ assert }) => {
const config = new Config();
config.key = 'cms';
config.version = 1;
config.data = cmsConfig as Record<string, any>;
await config.save();

assert.isTrue(config.$isPersisted);
assert.equal(config.key, 'cms');
assert.equal(config.version, 1);
assert.deepEqual(config.data, cmsConfig);
});

test('can retrieve stored cms config from configs table', async ({ assert }) => {
const config = new Config();
config.key = 'cms';
config.version = 1;
config.data = cmsConfig as Record<string, any>;
await config.save();

const retrieved = await Config.findOrFail(config.id);

assert.equal(retrieved.key, 'cms');
assert.equal(retrieved.version, 1);
assert.deepEqual(retrieved.data, cmsConfig);
assert.equal(retrieved.data.meta.name, 'Pilot CMS');
assert.equal(
retrieved.data.meta.logo,
'https://res.cloudinary.com/journeys/image/upload/v1756295658/logo_g8k7tf.png',
);
});

test('can store multiple versions of cms config', async ({ assert }) => {
const configV1 = new Config();
configV1.key = 'cms';
configV1.version = 1;
configV1.data = cmsConfig as Record<string, any>;
await configV1.save();

const modifiedConfig = {
...cmsConfig,
meta: { ...cmsConfig.meta, name: 'Updated Pilot CMS' },
};
const configV2 = new Config();
configV2.key = 'cms';
configV2.version = 2;
configV2.data = modifiedConfig as Record<string, any>;
await configV2.save();

const retrievedV1 = await Config.query()
.where({ key: 'cms', version: 1 })
.firstOrFail();
const retrievedV2 = await Config.query()
.where({ key: 'cms', version: 2 })
.firstOrFail();

assert.equal(retrievedV1.version, 1);
assert.equal(retrievedV1.data.meta.name, 'Pilot CMS');
assert.equal(retrievedV2.version, 2);
assert.equal(retrievedV2.data.meta.name, 'Updated Pilot CMS');
});
});