added format command and added trailing commas to reduce conflicts

This commit is contained in:
dswbx
2025-02-26 20:06:03 +01:00
parent 88b5359f1c
commit 7743f71a11
414 changed files with 3622 additions and 3610 deletions

View File

@@ -10,7 +10,7 @@ import {
entity,
json,
number,
text
text,
} from "../data/prototype";
import { MediaController } from "./api/MediaController";
import { ADAPTERS, buildMediaSchema, type mediaConfigSchema, registry } from "./media-schema";
@@ -52,12 +52,12 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
this.ensureSchema(
em({ [media.name as "media"]: media }, ({ index }, { media }) => {
index(media).on(["path"], true).on(["reference"]).on(["entity_id"]);
})
}),
);
} catch (e) {
console.error(e);
throw new Error(
`Could not build adapter with config ${JSON.stringify(this.config.adapter)}`
`Could not build adapter with config ${JSON.stringify(this.config.adapter)}`,
);
}
}
@@ -81,7 +81,7 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
mime_type: info.meta.type,
size: info.meta.size,
etag: info.etag,
modified_at: new Date()
modified_at: new Date(),
};
}
@@ -95,7 +95,7 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
modified_at: datetime(),
reference: text(),
entity_id: number(),
metadata: json()
metadata: json(),
};
getMediaEntity(forceCreate?: boolean): Entity<"media", typeof AppMedia.mediaFields> {
@@ -128,7 +128,7 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
mutator.__unstable_toggleSystemEntityCreation(true);
return { data };
},
{ mode: "sync", id: "add-data-media" }
{ mode: "sync", id: "add-data-media" },
);
// when file is deleted, sync with media entity
@@ -144,7 +144,7 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
console.log("App:storage:file deleted", e);
},
{ mode: "sync", id: "delete-data-media" }
{ mode: "sync", id: "delete-data-media" },
);
}
@@ -161,7 +161,7 @@ export class AppMedia extends Module<typeof mediaConfigSchema> {
return {
...this.config,
adapter: this.storage.getAdapter().toJSON(secrets)
adapter: this.storage.getAdapter().toJSON(secrets),
};
}
}

View File

@@ -6,9 +6,9 @@ export const mediaFieldConfigSchema = Type.Composite([
entity: Type.String(), // @todo: is this really required?
min_items: Type.Optional(Type.Number()),
max_items: Type.Optional(Type.Number()),
mime_types: Type.Optional(Type.Array(Type.String()))
mime_types: Type.Optional(Type.Array(Type.String())),
}),
baseFieldConfigSchema
baseFieldConfigSchema,
]);
export type MediaFieldConfig = Static<typeof mediaFieldConfigSchema>;
@@ -26,7 +26,7 @@ export type MediaItem = {
export class MediaField<
Required extends true | false = false,
TypeOverride = MediaItem[]
TypeOverride = MediaItem[],
> extends Field<MediaFieldConfig, TypeOverride, Required> {
override readonly type = "media";
@@ -64,7 +64,7 @@ export class MediaField<
type: "array",
items: { $ref },
minItems,
maxItems
maxItems,
};
}
}

View File

@@ -3,7 +3,7 @@ import {
type BaseModuleApiOptions,
ModuleApi,
type PrimaryFieldType,
type TInput
type TInput,
} from "modules/ModuleApi";
import type { FileWithPath } from "ui/elements/media/file-selector";
@@ -12,7 +12,7 @@ export type MediaApiOptions = BaseModuleApiOptions & {};
export class MediaApi extends ModuleApi<MediaApiOptions> {
protected override getDefaultOptions(): Partial<MediaApiOptions> {
return {
basepath: "/api/media"
basepath: "/api/media",
};
}
@@ -23,8 +23,8 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
getFile(filename: string) {
return this.get<ReadableStream<Uint8Array>>(["file", filename], undefined, {
headers: {
Accept: "*/*"
}
Accept: "*/*",
},
});
}
@@ -55,7 +55,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
getUploadHeaders(): Headers {
return new Headers({
Authorization: `Bearer ${this.options.token}`
Authorization: `Bearer ${this.options.token}`,
});
}
@@ -65,11 +65,11 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
filename?: string;
path?: TInput;
_init?: Omit<RequestInit, "body">;
}
},
) {
const headers = {
"Content-Type": "application/octet-stream",
...(opts?._init?.headers || {})
...(opts?._init?.headers || {}),
};
let name: string = opts?.filename || "";
try {
@@ -87,7 +87,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
const init = {
...(opts?._init || {}),
headers
headers,
};
if (opts?.path) {
return this.post(opts.path, body, init);
@@ -106,7 +106,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
filename?: string;
_init?: Omit<RequestInit, "body">;
path?: TInput;
} = {}
} = {},
) {
if (item instanceof Request || typeof item === "string") {
const res = await this.fetcher(item);
@@ -124,9 +124,9 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
...(opts._init ?? {}),
headers: {
...(opts._init?.headers ?? {}),
"Content-Type": item.headers.get("Content-Type") || "application/octet-stream"
}
}
"Content-Type": item.headers.get("Content-Type") || "application/octet-stream",
},
},
});
}
@@ -140,11 +140,11 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
item: Request | Response | string | File | ReadableStream,
opts?: {
_init?: Omit<RequestInit, "body">;
}
},
) {
return this.upload(item, {
...opts,
path: ["entity", entity, id, field]
path: ["entity", entity, id, field],
});
}

View File

@@ -65,7 +65,7 @@ export class MediaController extends Controller {
return c.json({
type: file?.type,
name: file?.name,
size: file?.size
size: file?.size,
});
});
}
@@ -82,7 +82,7 @@ export class MediaController extends Controller {
if (body.size > maxSize) {
return c.json(
{ error: `Max size (${maxSize} bytes) exceeded` },
HttpStatus.PAYLOAD_TOO_LARGE
HttpStatus.PAYLOAD_TOO_LARGE,
);
}
@@ -99,8 +99,8 @@ export class MediaController extends Controller {
tb(
"query",
Type.Object({
overwrite: Type.Optional(booleanLike)
})
overwrite: Type.Optional(booleanLike),
}),
),
async (c) => {
const entity_name = c.req.param("entity");
@@ -124,7 +124,7 @@ export class MediaController extends Controller {
const mediaRef = {
scope: field_name,
reference,
entity_id: entity_id
entity_id: entity_id,
};
// check max items
@@ -140,7 +140,7 @@ export class MediaController extends Controller {
if (!overwrite) {
return c.json(
{ error: `Max items (${max_items}) reached` },
HttpStatus.BAD_REQUEST
HttpStatus.BAD_REQUEST,
);
}
@@ -149,7 +149,7 @@ export class MediaController extends Controller {
if (count > max_items) {
return c.json(
{ error: `Max items (${max_items}) exceeded already with ${count} items.` },
HttpStatus.UNPROCESSABLE_ENTITY
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
@@ -159,9 +159,9 @@ export class MediaController extends Controller {
where: mediaRef,
sort: {
by: "id",
dir: "asc"
dir: "asc",
},
limit: count - max_items + 1
limit: count - max_items + 1,
});
if (deleteRes.data && deleteRes.data.length > 0) {
@@ -175,7 +175,7 @@ export class MediaController extends Controller {
if (!exists) {
return c.json(
{ error: `Entity "${entity_name}" with ID "${entity_id}" doesn't exist found` },
HttpStatus.NOT_FOUND
HttpStatus.NOT_FOUND,
);
}
@@ -186,7 +186,7 @@ export class MediaController extends Controller {
if (file.size > maxSize) {
return c.json(
{ error: `Max size (${maxSize} bytes) exceeded` },
HttpStatus.PAYLOAD_TOO_LARGE
HttpStatus.PAYLOAD_TOO_LARGE,
);
}
@@ -197,7 +197,7 @@ export class MediaController extends Controller {
mutator.__unstable_toggleSystemEntityCreation(false);
const result = await mutator.insertOne({
...this.media.uploadedEventDataToMediaPayload(info),
...mediaRef
...mediaRef,
} as any);
mutator.__unstable_toggleSystemEntityCreation(true);
@@ -210,7 +210,7 @@ export class MediaController extends Controller {
}
return c.json({ ok: true, result: result.data, ...info }, HttpStatus.CREATED);
}
},
);
return hono.all("*", (c) => c.notFound());

View File

@@ -8,12 +8,12 @@ export {
type StorageAdapter,
type FileMeta,
type FileListObject,
type StorageConfig
type StorageConfig,
} from "./storage/Storage";
import type { StorageAdapter } from "./storage/Storage";
import {
type CloudinaryConfig,
StorageCloudinaryAdapter
StorageCloudinaryAdapter,
} from "./storage/adapters/StorageCloudinaryAdapter";
import { type S3AdapterConfig, StorageS3Adapter } from "./storage/adapters/StorageS3Adapter";
@@ -30,7 +30,7 @@ export const MediaAdapterRegistry = new Registry<{
schema: TObject;
}>((cls: ClassThatImplements<StorageAdapter>) => ({
cls,
schema: cls.prototype.getSchema() as TObject
schema: cls.prototype.getSchema() as TObject,
}))
.register("s3", StorageS3Adapter)
.register("cloudinary", StorageCloudinaryAdapter);
@@ -38,10 +38,10 @@ export const MediaAdapterRegistry = new Registry<{
export const Adapters = {
s3: {
cls: StorageS3Adapter,
schema: StorageS3Adapter.prototype.getSchema()
schema: StorageS3Adapter.prototype.getSchema(),
},
cloudinary: {
cls: StorageCloudinaryAdapter,
schema: StorageCloudinaryAdapter.prototype.getSchema()
}
schema: StorageCloudinaryAdapter.prototype.getSchema(),
},
} as const;

View File

@@ -3,7 +3,7 @@ import { Adapters } from "media";
import { registries } from "modules/registries";
export const ADAPTERS = {
...Adapters
...Adapters,
} as const;
export const registry = registries.media;
@@ -13,13 +13,13 @@ export function buildMediaSchema() {
return Type.Object(
{
type: Const(name),
config: adapter.schema
config: adapter.schema,
},
{
title: adapter.schema?.title ?? name,
description: adapter.schema?.description,
additionalProperties: false
}
additionalProperties: false,
},
);
});
const adapterSchema = Type.Union(Object.values(adapterSchemaObject));
@@ -33,17 +33,17 @@ export function buildMediaSchema() {
{
body_max_size: Type.Optional(
Type.Number({
description: "Max size of the body in bytes. Leave blank for unlimited."
})
)
description: "Max size of the body in bytes. Leave blank for unlimited.",
}),
),
},
{ default: {} }
{ default: {} },
),
adapter: Type.Optional(adapterSchema)
adapter: Type.Optional(adapterSchema),
},
{
additionalProperties: false
}
additionalProperties: false,
},
);
}

View File

@@ -49,12 +49,12 @@ export class Storage implements EmitsEvents {
constructor(
adapter: StorageAdapter,
config: Partial<StorageConfig> = {},
emgr?: EventManager<any>
emgr?: EventManager<any>,
) {
this.#adapter = adapter;
this.config = {
...config,
body_max_size: config.body_max_size
body_max_size: config.body_max_size,
};
this.emgr = emgr ?? new EventManager();
@@ -78,7 +78,7 @@ export class Storage implements EmitsEvents {
async uploadFile(
file: FileBody,
name: string,
noEmit?: boolean
noEmit?: boolean,
): Promise<FileUploadedEventData> {
const result = await this.#adapter.putObject(name, file);
if (typeof result === "undefined") {
@@ -89,9 +89,9 @@ export class Storage implements EmitsEvents {
name,
meta: {
size: 0,
type: "application/octet-stream"
type: "application/octet-stream",
},
etag: typeof result === "string" ? result : ""
etag: typeof result === "string" ? result : "",
};
if (typeof result === "object") {
@@ -115,8 +115,8 @@ export class Storage implements EmitsEvents {
...info,
state: {
name: info.name,
path: info.name
}
path: info.name,
},
};
if (!noEmit) {
const result = await this.emgr.emit(new StorageEvents.FileUploadedEvent(eventData));

View File

@@ -7,9 +7,9 @@ export const cloudinaryAdapterConfig = Type.Object(
cloud_name: Type.String(),
api_key: Type.String(),
api_secret: Type.String(),
upload_preset: Type.Optional(Type.String())
upload_preset: Type.Optional(Type.String()),
},
{ title: "Cloudinary", description: "Cloudinary media storage" }
{ title: "Cloudinary", description: "Cloudinary media storage" },
);
export type CloudinaryConfig = Static<typeof cloudinaryAdapterConfig>;
@@ -80,7 +80,7 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
private getAuthorizationHeader() {
const credentials = btoa(`${this.config.api_key}:${this.config.api_secret}`);
return {
Authorization: `Basic ${credentials}`
Authorization: `Basic ${credentials}`,
};
}
@@ -102,12 +102,12 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
{
method: "POST",
headers: {
Accept: "application/json"
Accept: "application/json",
// content type must be undefined to use correct boundaries
//"Content-Type": "multipart/form-data",
},
body: formData
}
body: formData,
},
);
if (!result.ok) {
@@ -121,8 +121,8 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
etag: data.etag,
meta: {
type: this.getMimeType(data),
size: data.bytes
}
size: data.bytes,
},
};
}
@@ -133,9 +133,9 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
method: "GET",
headers: {
Accept: "application/json",
...this.getAuthorizationHeader()
}
}
...this.getAuthorizationHeader(),
},
},
);
if (!result.ok) {
@@ -146,7 +146,7 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
return data.resources.map((item) => ({
key: item.public_id,
last_modified: new Date(item.uploaded_at),
size: item.bytes
size: item.bytes,
}));
}
@@ -155,8 +155,8 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
return await fetch(url, {
method: "GET",
headers: {
Range: "bytes=0-1"
}
Range: "bytes=0-1",
},
});
}
@@ -172,7 +172,7 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
const size = Number(result.headers.get("content-range")?.split("/")[1]);
return {
type: type as string,
size: size
size: size,
};
}
@@ -182,7 +182,7 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
private guessType(key: string): string | undefined {
const extensions = {
image: ["jpg", "jpeg", "png", "gif", "webp", "svg"],
video: ["mp4", "webm", "ogg"]
video: ["mp4", "webm", "ogg"],
};
const ext = key.split(".").pop();
@@ -199,13 +199,13 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
async getObject(key: string, headers: Headers): Promise<Response> {
const res = await fetch(this.getObjectUrl(key), {
method: "GET",
headers: pickHeaders(headers, ["range"])
headers: pickHeaders(headers, ["range"]),
});
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: res.headers
headers: res.headers,
});
}
@@ -216,14 +216,14 @@ export class StorageCloudinaryAdapter implements StorageAdapter {
await fetch(`https://res.cloudinary.com/${this.config.cloud_name}/${type}/upload/`, {
method: "DELETE",
body: formData
body: formData,
});
}
toJSON(secrets?: boolean) {
return {
type: "cloudinary",
config: secrets ? this.config : { cloud_name: this.config.cloud_name }
config: secrets ? this.config : { cloud_name: this.config.cloud_name },
};
}
}

View File

@@ -5,15 +5,15 @@ import type {
FileListObject,
FileMeta,
FileUploadPayload,
StorageAdapter
StorageAdapter,
} from "../../Storage";
import { guess } from "../../mime-types-tiny";
export const localAdapterConfig = Type.Object(
{
path: Type.String({ default: "./" })
path: Type.String({ default: "./" }),
},
{ title: "Local", description: "Local file system storage" }
{ title: "Local", description: "Local file system storage" },
);
export type LocalAdapterConfig = Static<typeof localAdapterConfig>;
@@ -42,9 +42,9 @@ export class StorageLocalAdapter implements StorageAdapter {
return {
key: file,
last_modified: stats.mtime,
size: stats.size
size: stats.size,
};
})
}),
);
return fileStats;
}
@@ -95,8 +95,8 @@ export class StorageLocalAdapter implements StorageAdapter {
status: 200,
headers: {
"Content-Type": mimeType || "application/octet-stream",
"Content-Length": content.length.toString()
}
"Content-Length": content.length.toString(),
},
});
} catch (error) {
// Handle file reading errors
@@ -112,14 +112,14 @@ export class StorageLocalAdapter implements StorageAdapter {
const stats = await stat(`${this.config.path}/${key}`);
return {
type: guess(key) || "application/octet-stream",
size: stats.size
size: stats.size,
};
}
toJSON(secrets?: boolean) {
return {
type: this.getName(),
config: this.config
config: this.config,
};
}
}

View File

@@ -1,5 +1,5 @@
export {
StorageLocalAdapter,
type LocalAdapterConfig,
localAdapterConfig
localAdapterConfig,
} from "./StorageLocalAdapter";

View File

@@ -4,7 +4,7 @@ import type {
HeadObjectRequest,
ListObjectsV2Output,
ListObjectsV2Request,
PutObjectRequest
PutObjectRequest,
} from "@aws-sdk/client-s3";
import { AwsClient, isDebug } from "core";
import { type Static, Type, isFile, parse, pickHeaders } from "core/utils";
@@ -20,14 +20,14 @@ export const s3AdapterConfig = Type.Object(
description: "URL to S3 compatible endpoint without trailing slash",
examples: [
"https://{account_id}.r2.cloudflarestorage.com/{bucket}",
"https://{bucket}.s3.{region}.amazonaws.com"
]
})
"https://{bucket}.s3.{region}.amazonaws.com",
],
}),
},
{
title: "AWS S3",
description: "AWS S3 or compatible storage"
}
description: "AWS S3 or compatible storage",
},
);
export type S3AdapterConfig = Static<typeof s3AdapterConfig>;
@@ -40,12 +40,12 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
{
accessKeyId: config.access_key,
secretAccessKey: config.secret_access_key,
retries: isDebug() ? 0 : 10
retries: isDebug() ? 0 : 10,
},
{
convertParams: "pascalToKebab",
responseType: "xml"
}
responseType: "xml",
},
);
this.#config = parse(s3AdapterConfig, config);
}
@@ -78,12 +78,12 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
async listObjects(key: string = ""): Promise<FileListObject[]> {
const params: Omit<ListObjectsV2Request, "Bucket"> & { ListType: number } = {
ListType: 2,
Prefix: key
Prefix: key,
};
const url = this.getUrl("", params);
const res = await this.fetchJson<{ ListBucketResult: ListObjectsV2Output }>(url, {
method: "GET"
method: "GET",
});
// absolutely weird, but if only one object is there, it's an object, not an array
@@ -98,11 +98,11 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
acc.push({
key: obj.Key,
last_modified: obj.LastModified,
size: obj.Size
size: obj.Size,
});
}
},
[] as FileListObject[]
[] as FileListObject[],
);
return transformed;
@@ -112,12 +112,12 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
key: string,
body: FileBody,
// @todo: params must be added as headers, skipping for now
params: Omit<PutObjectRequest, "Bucket" | "Key"> = {}
params: Omit<PutObjectRequest, "Bucket" | "Key"> = {},
) {
const url = this.getUrl(key, {});
const res = await this.fetch(url, {
method: "PUT",
body
body,
});
if (res.ok) {
@@ -130,14 +130,14 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
private async headObject(
key: string,
params: Pick<HeadObjectRequest, "PartNumber" | "VersionId"> = {}
params: Pick<HeadObjectRequest, "PartNumber" | "VersionId"> = {},
) {
const url = this.getUrl(key, {});
return await this.fetch(url, {
method: "HEAD",
headers: {
Range: "bytes=0-1"
}
Range: "bytes=0-1",
},
});
}
@@ -148,7 +148,7 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
return {
type,
size
size,
};
}
@@ -159,7 +159,7 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
*/
async objectExists(
key: string,
params: Pick<HeadObjectRequest, "PartNumber" | "VersionId"> = {}
params: Pick<HeadObjectRequest, "PartNumber" | "VersionId"> = {},
) {
return (await this.headObject(key)).ok;
}
@@ -171,14 +171,14 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
const url = this.getUrl(key);
const res = await this.fetch(url, {
method: "GET",
headers: pickHeaders(headers, ["range"])
headers: pickHeaders(headers, ["range"]),
});
// Response has to be copied, because of middlewares that might set headers
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: res.headers
headers: res.headers,
});
}
@@ -187,18 +187,18 @@ export class StorageS3Adapter extends AwsClient implements StorageAdapter {
*/
async deleteObject(
key: string,
params: Omit<DeleteObjectRequest, "Bucket" | "Key"> = {}
params: Omit<DeleteObjectRequest, "Bucket" | "Key"> = {},
): Promise<void> {
const url = this.getUrl(key, params);
const res = await this.fetch(url, {
method: "DELETE"
method: "DELETE",
});
}
toJSON(secrets?: boolean) {
return {
type: this.getName(),
config: secrets ? this.#config : undefined
config: secrets ? this.#config : undefined,
};
}
}

View File

@@ -15,7 +15,7 @@ export class FileUploadedEvent extends Event<FileUploadedEventData, object> {
return this.clone({
// prepending result, so original is always kept
...data,
...this.params
...this.params,
});
}
}

View File

@@ -4,7 +4,7 @@ export const Q = {
image: ["jpeg", "png", "gif", "webp", "bmp", "tiff", "avif", "heic", "heif"],
text: ["html", "css", "mdx", "yaml", "vcard", "csv", "vtt"],
application: ["zip", "xml", "toml", "json", "json5"],
font: ["woff", "woff2", "ttf", "otf"]
font: ["woff", "woff2", "ttf", "otf"],
} as const;
// reduced
@@ -14,7 +14,7 @@ const c = {
t: (w = "plain") => `text/${w}`,
a: (w = "octet-stream") => `application/${w}`,
i: (w) => `image/${w}`,
v: (w) => `video/${w}`
v: (w) => `video/${w}`,
} as const;
export const M = new Map<string, string>([
["7z", c.z],
@@ -52,7 +52,7 @@ export const M = new Map<string, string>([
["webmanifest", c.a("manifest+json")],
["xls", c.a("vnd.ms-excel")],
["xlsx", `${c.vnd}.spreadsheetml.sheet`],
["yml", c.t("yaml")]
["yml", c.t("yaml")],
]);
export function guess(f: string): string {