init media adapter refactoring

This commit is contained in:
dswbx
2025-07-05 18:12:55 +02:00
parent c384bf4dd4
commit 786d1d1bd4
9 changed files with 86 additions and 101 deletions
+2 -8
View File
@@ -1,22 +1,15 @@
import { describe, expect, test } from "bun:test"; import { describe, expect, test } from "bun:test";
import { registries } from "../../src";
import { createApp } from "core/test/utils"; import { createApp } from "core/test/utils";
import { em, entity, text } from "../../src/data"; import { em, entity, text } from "../../src/data";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter"; import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { AppMedia } from "../../src/media/AppMedia"; import { AppMedia } from "../../src/media/AppMedia";
import { mediaConfigSchema } from "../../src/media/media-schema";
import { moduleTestSuite } from "./module-test-suite"; import { moduleTestSuite } from "./module-test-suite";
describe("AppMedia", () => { describe("AppMedia", () => {
test.only("...", () => {
const media = new AppMedia();
console.log(media.toJSON());
});
moduleTestSuite(AppMedia); moduleTestSuite(AppMedia);
test("should allow additional fields", async () => { test("should allow additional fields", async () => {
registries.media.register("local", StorageLocalAdapter); //registries.media.register("local", StorageLocalAdapter);
const app = createApp({ const app = createApp({
initialConfig: { initialConfig: {
@@ -37,6 +30,7 @@ describe("AppMedia", () => {
}).toJSON(), }).toJSON(),
}, },
}); });
app.module.media.adapters.set("local", StorageLocalAdapter);
await app.build(); await app.build();
+3 -3
View File
@@ -5,7 +5,6 @@ import type { CliBkndConfig, CliCommand } from "cli/types";
import { Option } from "commander"; import { Option } from "commander";
import { config } from "core"; import { config } from "core";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { registries } from "modules/registries";
import c from "picocolors"; import c from "picocolors";
import path from "node:path"; import path from "node:path";
import { import {
@@ -58,10 +57,11 @@ export const run: CliCommand = (program) => {
}; };
// automatically register local adapter // automatically register local adapter
const local = StorageLocalAdapter.prototype.getName(); // @todo: add back
/* const local = StorageLocalAdapter.prototype.getName();
if (!registries.media.has(local)) { if (!registries.media.has(local)) {
registries.media.register(local, StorageLocalAdapter); registries.media.register(local, StorageLocalAdapter);
} } */
type MakeAppConfig = { type MakeAppConfig = {
connection?: CreateAppConfig["connection"]; connection?: CreateAppConfig["connection"];
+1 -1
View File
@@ -29,7 +29,7 @@ export class SchemaObject<Schema extends TSchema = TSchema> {
this._default = _schema.template({}, { withOptional: true }) as any; this._default = _schema.template({}, { withOptional: true }) as any;
this._value = parse(_schema, structuredClone(initial ?? {}), { this._value = parse(_schema, structuredClone(initial ?? {}), {
withDefaults: true, withDefaults: true,
withExtendedDefaults: true, //withExtendedDefaults: true,
forceParse: this.isForceParse(), forceParse: this.isForceParse(),
skipMark: this.isForceParse(), skipMark: this.isForceParse(),
}); });
-1
View File
@@ -34,7 +34,6 @@ export type { ServerEnv } from "modules/Controller";
export type { BkndConfig } from "bknd/adapter"; export type { BkndConfig } from "bknd/adapter";
export * as middlewares from "modules/middlewares"; export * as middlewares from "modules/middlewares";
export { registries } from "modules/registries";
export type { MediaFieldSchema } from "media/AppMedia"; export type { MediaFieldSchema } from "media/AppMedia";
export type { UserFieldSchema } from "auth/AppAuth"; export type { UserFieldSchema } from "auth/AppAuth";
+50 -5
View File
@@ -1,12 +1,15 @@
import type { AppEntity } from "core"; import type { AppEntity, Constructor } from "core";
import { $console } from "core/utils"; import { $console, objectTransform } from "core/utils";
import type { Entity, EntityManager } from "data"; import type { Entity, EntityManager } from "data";
import { type FileUploadedEventData, Storage, type StorageAdapter, MediaPermissions } from "media"; import { type FileUploadedEventData, Storage, type StorageAdapter, MediaPermissions } from "media";
import { Module } from "modules/Module"; import { Module } from "modules/Module";
import { type FieldSchema, em, entity } from "../data/prototype"; import { type FieldSchema, em, entity } from "../data/prototype";
import { MediaController } from "./api/MediaController"; import { MediaController } from "./api/MediaController";
import { buildMediaSchema, type mediaConfigSchema, registry, type TAppMediaConfig } from "./media-schema"; import { mediaConfigSchema, type TAppMediaConfig } from "./media-schema";
import { mediaFields } from "./media-entities"; import { mediaFields } from "./media-entities";
import { StorageS3Adapter } from "media/storage/adapters/s3/StorageS3Adapter";
import { StorageCloudinaryAdapter } from "media/storage/adapters/cloudinary/StorageCloudinaryAdapter";
import { s } from "core/object/schema";
export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>; export type MediaFieldSchema = FieldSchema<typeof AppMedia.mediaFields>;
declare module "core" { declare module "core" {
@@ -15,10 +18,12 @@ declare module "core" {
media: Media; media: Media;
} }
} }
type ClassThatImplements<T> = Constructor<T> & { prototype: T };
// @todo: current workaround to make it all required // @todo: current workaround to make it all required
export class AppMedia extends Module<Required<TAppMediaConfig>> { export class AppMedia extends Module<Required<TAppMediaConfig>> {
private _storage?: Storage; private _storage?: Storage;
adapters: Map<string, ClassThatImplements<StorageAdapter>> = new Map();
override async build() { override async build() {
if (!this.config.enabled) { if (!this.config.enabled) {
@@ -26,6 +31,15 @@ export class AppMedia extends Module<Required<TAppMediaConfig>> {
return; return;
} }
// register default adapters
if (!this.adapters.has("s3")) {
this.adapters.set("s3", StorageS3Adapter);
}
if (!this.adapters.has("cloudinary")) {
this.adapters.set("cloudinary", StorageCloudinaryAdapter);
}
if (!this.config.adapter) { if (!this.config.adapter) {
console.info("No storage adapter provided, skip building media."); console.info("No storage adapter provided, skip building media.");
return; return;
@@ -35,7 +49,11 @@ export class AppMedia extends Module<Required<TAppMediaConfig>> {
let adapter: StorageAdapter; let adapter: StorageAdapter;
try { try {
const { type, config } = this.config.adapter; const { type, config } = this.config.adapter;
const cls = registry.get(type as any).cls; const cls = this.adapters.get(type as any);
if (!cls) {
throw new Error(`Adapter ${type} not found`);
}
adapter = new cls(config as any); adapter = new cls(config as any);
this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr); this._storage = new Storage(adapter, this.config.storage, this.ctx.emgr);
@@ -59,7 +77,34 @@ export class AppMedia extends Module<Required<TAppMediaConfig>> {
} }
getSchema() { getSchema() {
return buildMediaSchema(); if (!this.adapters) {
return mediaConfigSchema;
}
const adapterSchemaObject = objectTransform(
Object.fromEntries(this.adapters.entries()),
(adapter, name) => {
const schema = adapter.prototype.getSchema();
if (!schema) {
throw new Error(`Adapter ${name} has no schema`);
}
return s.strictObject(
{
type: s.literal(name),
config: schema,
},
{
title: String(schema.title ?? name),
description: schema.description,
},
);
},
);
return s.strictObject({
...mediaConfigSchema.properties,
adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(),
});
} }
get basepath() { get basepath() {
-26
View File
@@ -1,5 +1,3 @@
import { type Constructor, Registry } from "core";
export { guess as guessMimeType } from "./storage/mime-types-tiny"; export { guess as guessMimeType } from "./storage/mime-types-tiny";
export { export {
Storage, Storage,
@@ -15,7 +13,6 @@ import {
StorageCloudinaryAdapter, StorageCloudinaryAdapter,
} from "./storage/adapters/cloudinary/StorageCloudinaryAdapter"; } from "./storage/adapters/cloudinary/StorageCloudinaryAdapter";
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter"; import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/s3/StorageS3Adapter";
import type { s } from "core/object/schema";
export { StorageAdapter }; export { StorageAdapter };
export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig }; export { StorageS3Adapter, type S3AdapterConfig, StorageCloudinaryAdapter, type CloudinaryConfig };
@@ -25,27 +22,4 @@ export * as MediaPermissions from "./media-permissions";
export type { FileUploadedEventData } from "./storage/events"; export type { FileUploadedEventData } from "./storage/events";
export * from "./utils"; export * from "./utils";
type ClassThatImplements<T> = Constructor<T> & { prototype: T };
export const MediaAdapterRegistry = new Registry<{
cls: ClassThatImplements<StorageAdapter>;
schema: s.Schema;
}>((cls: ClassThatImplements<StorageAdapter>) => ({
cls,
schema: cls.prototype.getSchema() as s.Schema,
}))
.register("s3", StorageS3Adapter)
.register("cloudinary", StorageCloudinaryAdapter);
export const Adapters = {
s3: {
cls: StorageS3Adapter,
schema: StorageS3Adapter.prototype.getSchema(),
},
cloudinary: {
cls: StorageCloudinaryAdapter,
schema: StorageCloudinaryAdapter.prototype.getSchema(),
},
} as const;
export { adapterTestSuite } from "./storage/adapters/adapter-test-suite"; export { adapterTestSuite } from "./storage/adapters/adapter-test-suite";
+7 -28
View File
@@ -1,29 +1,6 @@
import { objectTransform } from "core/utils";
import { Adapters } from "media";
import { registries } from "modules/registries";
import { s } from "core/object/schema"; import { s } from "core/object/schema";
export const ADAPTERS = { export const mediaConfigSchema = s.strictObject(
...Adapters,
} as const;
export const registry = registries.media;
export function buildMediaSchema() {
const adapterSchemaObject = objectTransform(registry.all(), (adapter, name) => {
return s.strictObject(
{
type: s.literal(name),
config: adapter.schema,
},
{
title: adapter.schema?.title ?? name,
description: adapter.schema?.description,
},
);
});
return s.strictObject(
{ {
enabled: s.boolean({ default: false }), enabled: s.boolean({ default: false }),
basepath: s.string({ default: "/api/media" }), basepath: s.string({ default: "/api/media" }),
@@ -38,13 +15,15 @@ export function buildMediaSchema() {
}, },
{ default: {} }, { default: {} },
), ),
adapter: s.anyOf(Object.values(adapterSchemaObject)).optional(), adapter: s
.strictObject({
type: s.string(),
config: s.any(),
})
.optional(),
}, },
{ {
default: {}, default: {},
}, },
); );
}
export const mediaConfigSchema = buildMediaSchema();
export type TAppMediaConfig = s.Static<typeof mediaConfigSchema>; export type TAppMediaConfig = s.Static<typeof mediaConfigSchema>;
+6 -5
View File
@@ -20,7 +20,7 @@ import { AppMedia } from "../media/AppMedia";
import type { ServerEnv } from "./Controller"; import type { ServerEnv } from "./Controller";
import { Module, type ModuleBuildContext } from "./Module"; import { Module, type ModuleBuildContext } from "./Module";
import { ModuleHelper } from "./ModuleHelper"; import { ModuleHelper } from "./ModuleHelper";
import { s, mark, stripMark } from "core/object/schema"; import { s } from "core/object/schema";
export type { ModuleBuildContext }; export type { ModuleBuildContext };
@@ -166,7 +166,8 @@ export class ModuleManager {
if ("version" in options.initial) { if ("version" in options.initial) {
const { version, ...initialConfig } = options.initial; const { version, ...initialConfig } = options.initial;
this._version = version; this._version = version;
initial = stripMark(initialConfig); //initial = stripMark(initialConfig);
initial = initialConfig;
this._booted_with = "provided"; this._booted_with = "provided";
} else { } else {
@@ -487,10 +488,11 @@ export class ModuleManager {
// set version and config from fetched // set version and config from fetched
this._version = result.version; this._version = result.version;
if (this.options?.trustFetched === true) { // @todo: add back?
/* if (this.options?.trustFetched === true) {
this.logger.log("trusting fetched config (mark)"); this.logger.log("trusting fetched config (mark)");
mark(result.json); mark(result.json);
} } */
// if version doesn't match, migrate before building // if version doesn't match, migrate before building
if (this.version() !== CURRENT_VERSION) { if (this.version() !== CURRENT_VERSION) {
@@ -732,7 +734,6 @@ export function getDefaultSchema() {
export function getDefaultConfig(): ModuleConfigs { export function getDefaultConfig(): ModuleConfigs {
const config = transformObject(MODULES, (module) => { const config = transformObject(MODULES, (module) => {
return module.prototype.getSchema().template(); return module.prototype.getSchema().template();
//return Default(module.prototype.getSchema(), {});
}); });
return config as any; return config as any;
-7
View File
@@ -1,7 +0,0 @@
import { MediaAdapterRegistry } from "media";
const registries = {
media: MediaAdapterRegistry,
} as const;
export { registries };