appos 0.2.1 → 0.2.3-0
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.
- package/dist/bin/auth-schema-7KeUwlcd.mjs +2 -0
- package/dist/bin/concurrently.mjs +2 -0
- package/dist/bin/event-v2sCJkNd.mjs +2 -0
- package/dist/bin/extract-blob-metadata-TqNd9w-6.mjs +2 -0
- package/dist/bin/generate-image-variant-D8H9FxgD.mjs +2 -0
- package/dist/bin/generate-preview-5jLZLX6I.mjs +2 -0
- package/dist/bin/main.mjs +362 -0
- package/dist/bin/purge-attachment-CMlJMNOk.mjs +2 -0
- package/dist/bin/purge-audit-logs-hd6q6vnR.mjs +2 -0
- package/dist/bin/purge-unattached-blobs-BYv5b9R9.mjs +2 -0
- package/dist/bin/track-db-changes-q0Vl7Htm.mjs +2 -0
- package/dist/bin/vite.mjs +2 -0
- package/dist/bin/vitest.mjs +2 -0
- package/dist/bin/workflow-BagSlsMp.mjs +2 -0
- package/dist/bin/youch-handler-Jj6i1XIT.mjs +2 -0
- package/dist/exports/api/_virtual/rolldown_runtime.mjs +1 -0
- package/dist/exports/api/app-context.d.mts +115 -0
- package/dist/exports/api/app-context.mjs +1 -0
- package/dist/exports/api/auth-schema.d.mts +4248 -0
- package/dist/exports/api/auth-schema.mjs +1 -0
- package/dist/exports/api/auth.d.mts +398 -0
- package/dist/exports/api/auth.mjs +1 -0
- package/dist/exports/api/cache.d.mts +44 -0
- package/dist/exports/api/cache.mjs +1 -0
- package/dist/exports/api/config.d.mts +28 -0
- package/dist/exports/api/config.mjs +1 -0
- package/dist/exports/api/container.d.mts +210 -0
- package/dist/exports/api/container.mjs +1 -0
- package/dist/exports/api/database.d.mts +99 -0
- package/dist/exports/api/database.mjs +1 -0
- package/dist/exports/api/event.d.mts +235 -0
- package/dist/exports/api/event.mjs +1 -0
- package/dist/exports/api/i18n.d.mts +34 -0
- package/dist/exports/api/i18n.mjs +1 -0
- package/dist/exports/api/index.d.mts +21 -0
- package/dist/exports/api/index.mjs +1 -0
- package/dist/exports/api/logger.d.mts +21 -0
- package/dist/exports/api/logger.mjs +1 -0
- package/dist/exports/api/mailer.d.mts +70 -0
- package/dist/exports/api/mailer.mjs +1 -0
- package/dist/exports/api/middleware/request-logger.d.mts +24 -0
- package/dist/exports/api/middleware.d.mts +39 -0
- package/dist/exports/api/middleware.mjs +1 -0
- package/dist/exports/api/node_modules/.bun/change-case@5.4.4/node_modules/change-case/dist/index.mjs +1 -0
- package/dist/exports/api/openapi.d.mts +271 -0
- package/dist/exports/api/openapi.mjs +1 -0
- package/dist/exports/api/orm.d.mts +13 -0
- package/dist/exports/api/orm.mjs +1 -0
- package/dist/exports/api/otel.d.mts +40 -0
- package/dist/exports/api/otel.mjs +1 -0
- package/dist/exports/api/packages/appos/src/constants.mjs +1 -0
- package/dist/exports/api/packages/appos/src/instrumentation.d.mts +7 -0
- package/dist/exports/api/packages/appos/src/instrumentation.mjs +1 -0
- package/dist/exports/api/packages/appos/src/web/auth.mjs +1 -0
- package/dist/exports/api/redis.d.mts +34 -0
- package/dist/exports/api/redis.mjs +1 -0
- package/dist/exports/api/storage-schema.d.mts +707 -0
- package/dist/exports/api/storage-schema.mjs +1 -0
- package/dist/exports/api/storage.d.mts +506 -0
- package/dist/exports/api/storage.mjs +1 -0
- package/dist/exports/api/workflow.d.mts +250 -0
- package/dist/exports/api/workflow.mjs +1 -0
- package/dist/exports/api/workflows/_virtual/rolldown_runtime.mjs +1 -0
- package/dist/exports/api/workflows/auth-schema.mjs +1 -0
- package/dist/exports/api/workflows/auth.d.mts +375 -0
- package/dist/exports/api/workflows/cache.d.mts +44 -0
- package/dist/exports/api/workflows/config.d.mts +18 -0
- package/dist/exports/api/workflows/container.d.mts +167 -0
- package/dist/exports/api/workflows/database.d.mts +46 -0
- package/dist/exports/api/workflows/event.d.mts +68 -0
- package/dist/exports/api/workflows/event.mjs +1 -0
- package/dist/exports/api/workflows/extract-blob-metadata.mjs +1 -0
- package/dist/exports/api/workflows/generate-image-variant.d.mts +63 -0
- package/dist/exports/api/workflows/generate-image-variant.mjs +1 -0
- package/dist/exports/api/workflows/generate-preview.mjs +1 -0
- package/dist/exports/api/workflows/index.d.mts +2 -0
- package/dist/exports/api/workflows/index.mjs +1 -0
- package/dist/exports/api/workflows/logger.d.mts +21 -0
- package/dist/exports/api/workflows/mailer.d.mts +70 -0
- package/dist/exports/api/workflows/orm.d.mts +13 -0
- package/dist/exports/api/workflows/purge-attachment.mjs +1 -0
- package/dist/exports/api/workflows/purge-audit-logs.mjs +1 -0
- package/dist/exports/api/workflows/purge-unattached-blobs.mjs +1 -0
- package/dist/exports/api/workflows/redis.mjs +1 -0
- package/dist/exports/api/workflows/storage-schema.d.mts +699 -0
- package/dist/exports/api/workflows/storage.d.mts +396 -0
- package/dist/exports/api/workflows/track-db-changes.d.mts +72 -0
- package/dist/exports/api/workflows/track-db-changes.mjs +1 -0
- package/dist/exports/api/workflows/workflow.d.mts +24 -0
- package/dist/exports/api/workflows/workflow.mjs +1 -0
- package/dist/exports/cli/_virtual/rolldown_runtime.mjs +1 -0
- package/dist/exports/cli/api/auth-schema.mjs +1 -0
- package/dist/exports/cli/api/auth.d.mts +375 -0
- package/dist/exports/cli/api/cache.d.mts +44 -0
- package/dist/exports/cli/api/config.d.mts +18 -0
- package/dist/exports/cli/api/container.d.mts +167 -0
- package/dist/exports/cli/api/database.d.mts +46 -0
- package/dist/exports/cli/api/event.d.mts +68 -0
- package/dist/exports/cli/api/event.mjs +1 -0
- package/dist/exports/cli/api/logger.d.mts +21 -0
- package/dist/exports/cli/api/mailer.d.mts +70 -0
- package/dist/exports/cli/api/orm.d.mts +13 -0
- package/dist/exports/cli/api/redis.mjs +1 -0
- package/dist/exports/cli/api/storage-schema.d.mts +699 -0
- package/dist/exports/cli/api/storage.d.mts +396 -0
- package/dist/exports/cli/api/workflow.d.mts +2 -0
- package/dist/exports/cli/api/workflow.mjs +1 -0
- package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +1 -0
- package/dist/exports/cli/api/workflows/generate-image-variant.d.mts +63 -0
- package/dist/exports/cli/api/workflows/generate-image-variant.mjs +1 -0
- package/dist/exports/cli/api/workflows/generate-preview.mjs +1 -0
- package/dist/exports/cli/api/workflows/purge-attachment.mjs +1 -0
- package/dist/exports/cli/api/workflows/purge-audit-logs.mjs +1 -0
- package/dist/exports/cli/api/workflows/purge-unattached-blobs.mjs +1 -0
- package/dist/exports/cli/api/workflows/track-db-changes.mjs +1 -0
- package/dist/exports/cli/command.d.mts +54 -0
- package/dist/exports/cli/command.mjs +1 -0
- package/dist/exports/cli/context.d.mts +170 -0
- package/dist/exports/cli/index.d.mts +3 -0
- package/dist/exports/cli/index.mjs +1 -0
- package/dist/exports/devtools/index.d.ts +3 -0
- package/dist/exports/devtools/index.js +1 -0
- package/dist/exports/instrumentation.d.mts +1 -0
- package/dist/exports/instrumentation.mjs +1 -0
- package/dist/exports/tests/_virtual/rolldown_runtime.mjs +1 -0
- package/dist/exports/tests/api.d.mts +86 -0
- package/dist/exports/tests/api.mjs +1 -0
- package/dist/exports/tests/mock.d.mts +1 -0
- package/dist/exports/tests/mock.mjs +1 -0
- package/dist/exports/tests/node_modules/.bun/change-case@5.4.4/node_modules/change-case/dist/index.mjs +1 -0
- package/dist/exports/tests/node_modules/.bun/rate-limit-redis@4.3.1_f1fa5524233c9c60/node_modules/rate-limit-redis/dist/index.mjs +25 -0
- package/dist/exports/tests/packages/appos/src/api/app-context.d.mts +115 -0
- package/dist/exports/tests/packages/appos/src/api/auth-schema.d.mts +4248 -0
- package/dist/exports/tests/packages/appos/src/api/auth-schema.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/auth.d.mts +398 -0
- package/dist/exports/tests/packages/appos/src/api/cache.d.mts +44 -0
- package/dist/exports/tests/packages/appos/src/api/config.d.mts +28 -0
- package/dist/exports/tests/packages/appos/src/api/container.d.mts +210 -0
- package/dist/exports/tests/packages/appos/src/api/database.d.mts +99 -0
- package/dist/exports/tests/packages/appos/src/api/database.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/event.d.mts +235 -0
- package/dist/exports/tests/packages/appos/src/api/event.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/i18n.d.mts +34 -0
- package/dist/exports/tests/packages/appos/src/api/index.d.mts +27 -0
- package/dist/exports/tests/packages/appos/src/api/logger.d.mts +21 -0
- package/dist/exports/tests/packages/appos/src/api/mailer.d.mts +70 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/error-handler.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/health.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/i18n.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/request-logger.d.mts +24 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/request-logger.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/shutdown.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/timeout.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/middleware/youch-handler.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/middleware.d.mts +39 -0
- package/dist/exports/tests/packages/appos/src/api/middleware.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/openapi.d.mts +271 -0
- package/dist/exports/tests/packages/appos/src/api/orm.d.mts +13 -0
- package/dist/exports/tests/packages/appos/src/api/otel.d.mts +40 -0
- package/dist/exports/tests/packages/appos/src/api/redis.d.mts +34 -0
- package/dist/exports/tests/packages/appos/src/api/redis.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/server.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/storage-schema.d.mts +707 -0
- package/dist/exports/tests/packages/appos/src/api/storage.d.mts +506 -0
- package/dist/exports/tests/packages/appos/src/api/workflow.d.mts +250 -0
- package/dist/exports/tests/packages/appos/src/api/workflow.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/extract-blob-metadata.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/generate-image-variant.d.mts +99 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/generate-image-variant.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/generate-preview.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/purge-attachment.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/purge-audit-logs.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/purge-unattached-blobs.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/api/workflows/track-db-changes.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/constants.mjs +1 -0
- package/dist/exports/tests/packages/appos/src/instrumentation.d.mts +7 -0
- package/dist/exports/tests/packages/appos/src/instrumentation.mjs +1 -0
- package/dist/exports/tests/react.d.mts +2 -0
- package/dist/exports/tests/react.mjs +1 -0
- package/dist/exports/tests/setup.d.mts +1 -0
- package/dist/exports/tests/setup.mjs +1 -0
- package/dist/exports/vendors/date.js +1 -0
- package/dist/exports/vendors/toolkit.js +1 -0
- package/dist/exports/vendors/zod.d.ts +1 -0
- package/dist/exports/vendors/zod.js +1 -0
- package/dist/exports/vite/index.d.mts +19 -0
- package/dist/exports/vite/index.mjs +1 -0
- package/dist/exports/vitest/config.d.mts +1 -0
- package/dist/exports/vitest/config.mjs +1 -0
- package/dist/exports/vitest/globals.d.mts +1 -0
- package/dist/exports/vitest/globals.mjs +1 -0
- package/dist/exports/vitest/index.d.mts +1 -0
- package/dist/exports/vitest/index.mjs +1 -0
- package/dist/exports/web/api/auth.d.ts +125 -0
- package/dist/exports/web/api/database.d.ts +4 -0
- package/dist/exports/web/api/logger.d.ts +1 -0
- package/dist/exports/web/auth.d.ts +2388 -0
- package/dist/exports/web/auth.js +1 -0
- package/dist/exports/web/i18n.d.ts +42 -0
- package/dist/exports/web/i18n.js +1 -0
- package/dist/exports/web/index.d.ts +6 -0
- package/dist/exports/web/index.js +1 -0
- package/package.json +138 -98
- package/build/bin/main.mjs +0 -2
- package/build/exports/cli/index.d.mts +0 -325
- package/build/exports/cli/index.mjs +0 -1
- package/build/exports/instrumentation/execAsync-DaIUcs6_.mjs +0 -1
- package/build/exports/instrumentation/getMachineId-bsd-bB6ipDhm.mjs +0 -1
- package/build/exports/instrumentation/getMachineId-darwin-D1Bx5aCe.mjs +0 -2
- package/build/exports/instrumentation/getMachineId-linux-D_R9Tla0.mjs +0 -1
- package/build/exports/instrumentation/getMachineId-unsupported-BZKPE_Ev.mjs +0 -1
- package/build/exports/instrumentation/getMachineId-win-CmPvIqHL.mjs +0 -1
- package/build/exports/instrumentation/instrumentation.d.mts +0 -1
- package/build/exports/instrumentation/instrumentation.mjs +0 -80
- package/build/exports/server/index.d.mts +0 -327
- package/build/exports/server/index.mjs +0 -219
- package/build/exports/server/react-gPO8Jsy-.mjs +0 -13
- package/build/exports/server/server.node-D_9RYjm9.mjs +0 -210
- package/build/exports/store/index.d.mts +0 -58
- package/build/exports/store/index.mjs +0 -15
- package/build/exports/support/datetime.js +0 -1
- package/build/exports/support/utils.js +0 -1
- package/build/exports/support/zod.d.ts +0 -2
- package/build/exports/support/zod.js +0 -23
- package/build/exports/test/dist-DAsoCGWk.mjs +0 -348
- package/build/exports/test/index.d.mts +0 -3
- package/build/exports/test/index.mjs +0 -1
- package/build/exports/test/magic-string.es-BWgiB2kd.mjs +0 -14
- package/build/exports/test/setup.d.mts +0 -1
- package/build/exports/test/setup.mjs +0 -329
- /package/{build/exports/support/datetime.d.ts → dist/exports/vendors/date.d.ts} +0 -0
- /package/{build/exports/support/utils.d.ts → dist/exports/vendors/toolkit.d.ts} +0 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { StorageBlob, StorageRelationsConfig, StorageTables } from "./storage-schema.mjs";
|
|
2
|
+
import { ImageTransformations, transformationsSchema } from "./workflows/generate-image-variant.mjs";
|
|
3
|
+
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
|
+
import { Pool } from "pg";
|
|
5
|
+
import { DriveManager } from "flydrive";
|
|
6
|
+
import { FSDriverOptions } from "flydrive/drivers/fs/types";
|
|
7
|
+
import { S3DriverOptions } from "flydrive/drivers/s3/types";
|
|
8
|
+
|
|
9
|
+
//#region src/api/storage.d.ts
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Database type with storage schema and relations.
|
|
13
|
+
*/
|
|
14
|
+
type DatabaseWithStorage = NodePgDatabase<StorageTables, StorageRelationsConfig> & {
|
|
15
|
+
$client: Pool;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Type of the storage service instance.
|
|
19
|
+
*/
|
|
20
|
+
type Storage<TDisks extends Record<string, FSDriverOptions | S3DriverOptions> = Record<string, FSDriverOptions | S3DriverOptions>, TTableNames extends string = string, TAttachments extends Partial<Record<TTableNames, readonly string[]>> = Record<never, never>> = StorageService<Extract<keyof TDisks, string>, TTableNames, TAttachments>;
|
|
21
|
+
/**
|
|
22
|
+
* Helper type to extract attachment name for a specific table.
|
|
23
|
+
*/
|
|
24
|
+
type AttachmentName<TAttachments extends Partial<Record<string, readonly string[]>>, TTable extends string> = TTable extends keyof TAttachments ? TAttachments[TTable] extends readonly (infer N)[] ? N : string : string;
|
|
25
|
+
/**
|
|
26
|
+
* Storage service for blob operations.
|
|
27
|
+
*
|
|
28
|
+
* This service uses a dual-drive architecture when configured:
|
|
29
|
+
* - `drive`: For actual file operations (upload, delete, read) using internal endpoints
|
|
30
|
+
* - `signedUrlDrive`: For generating browser-accessible signed URLs with public endpoints
|
|
31
|
+
*
|
|
32
|
+
* This allows containers to use fast internal network for operations while
|
|
33
|
+
* generating URLs that work in end-user browsers.
|
|
34
|
+
*
|
|
35
|
+
* @template TDiskNames - Union of disk names (e.g., "private" | "public")
|
|
36
|
+
* @template TTableNames - Union of table names for type-safe attachment methods
|
|
37
|
+
* @template TAttachments - Mapping of table names to valid attachment names
|
|
38
|
+
*/
|
|
39
|
+
declare class StorageService<TDiskNames extends string = string, TTableNames extends string = string, TAttachments extends Partial<Record<TTableNames, readonly string[]>> = Record<never, never>> {
|
|
40
|
+
/**
|
|
41
|
+
* The drive manager for file operations.
|
|
42
|
+
*/
|
|
43
|
+
readonly drive: DriveManager<any>;
|
|
44
|
+
/**
|
|
45
|
+
* The database instance with storage schema and relations.
|
|
46
|
+
*/
|
|
47
|
+
readonly db: DatabaseWithStorage;
|
|
48
|
+
/**
|
|
49
|
+
* The default disk name.
|
|
50
|
+
*/
|
|
51
|
+
readonly defaultDisk: TDiskNames;
|
|
52
|
+
/**
|
|
53
|
+
* Cron expression for unattached blob purge schedule.
|
|
54
|
+
*
|
|
55
|
+
* @default "0 0 * * *"
|
|
56
|
+
*/
|
|
57
|
+
readonly purgeCron?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Secret key for signing blob IDs.
|
|
60
|
+
*/
|
|
61
|
+
readonly secret?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Drive manager for generating signed URLs (public endpoint).
|
|
64
|
+
*/
|
|
65
|
+
readonly signedUrlDrive: DriveManager<any>;
|
|
66
|
+
constructor(drive: DriveManager<any>, db: DatabaseWithStorage, defaultDisk: TDiskNames, signedUrlDrive: DriveManager<any>, purgeCron?: string, secret?: string);
|
|
67
|
+
/**
|
|
68
|
+
* Create a blob record and upload file.
|
|
69
|
+
*/
|
|
70
|
+
createBlob(file: Buffer | Uint8Array, options: {
|
|
71
|
+
filename: string;
|
|
72
|
+
contentType?: string;
|
|
73
|
+
metadata?: Record<string, any>;
|
|
74
|
+
serviceName?: TDiskNames;
|
|
75
|
+
prefix?: string;
|
|
76
|
+
}): Promise<StorageBlob>;
|
|
77
|
+
/**
|
|
78
|
+
* Get blob by ID.
|
|
79
|
+
*/
|
|
80
|
+
getBlob(id: string): Promise<StorageBlob | null>;
|
|
81
|
+
/**
|
|
82
|
+
* Download blob content.
|
|
83
|
+
*/
|
|
84
|
+
downloadBlob(id: string): Promise<Buffer | null>;
|
|
85
|
+
/**
|
|
86
|
+
* Delete blob and its content.
|
|
87
|
+
*/
|
|
88
|
+
deleteBlob(id: string): Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Get signed URL for blob. If blob is on public service, returns permanent
|
|
91
|
+
* URL instead.
|
|
92
|
+
*/
|
|
93
|
+
getSignedUrl(id: string, options?: {
|
|
94
|
+
expiresIn?: number;
|
|
95
|
+
disposition?: "inline" | "attachment";
|
|
96
|
+
filename?: string;
|
|
97
|
+
}): Promise<string | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Get permanent public URL (no expiration).
|
|
100
|
+
* Works for blobs on public storage service.
|
|
101
|
+
*/
|
|
102
|
+
getPublicUrl(id: string): Promise<string | null>;
|
|
103
|
+
/**
|
|
104
|
+
* Create attachment between a record and blob.
|
|
105
|
+
*
|
|
106
|
+
* @param recordType - The table name (e.g., 'users')
|
|
107
|
+
* @param recordId - The record ID
|
|
108
|
+
* @param blobId - The blob ID to attach
|
|
109
|
+
* @param name - The attachment name (e.g., 'avatar')
|
|
110
|
+
* @param replaceExisting - If true, replaces existing attachment with same name (for one-to-one)
|
|
111
|
+
*/
|
|
112
|
+
createAttachment(recordType: string, recordId: string, blobId: string, name: string, replaceExisting?: boolean): Promise<{
|
|
113
|
+
id: string;
|
|
114
|
+
createdAt: string;
|
|
115
|
+
name: string;
|
|
116
|
+
recordType: string;
|
|
117
|
+
recordId: string;
|
|
118
|
+
blobId: string;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Get attachments for a record with their associated blobs.
|
|
122
|
+
*/
|
|
123
|
+
getAttachments(recordType: string, recordId: string, name?: string): Promise<{
|
|
124
|
+
id: string;
|
|
125
|
+
createdAt: string;
|
|
126
|
+
name: string;
|
|
127
|
+
recordType: string;
|
|
128
|
+
recordId: string;
|
|
129
|
+
blobId: string;
|
|
130
|
+
blob: {
|
|
131
|
+
id: string;
|
|
132
|
+
key: string;
|
|
133
|
+
filename: string;
|
|
134
|
+
contentType: string | null;
|
|
135
|
+
metadata: unknown;
|
|
136
|
+
serviceName: string;
|
|
137
|
+
byteSize: number;
|
|
138
|
+
checksum: string | null;
|
|
139
|
+
createdAt: string;
|
|
140
|
+
} | null;
|
|
141
|
+
}[]>;
|
|
142
|
+
/**
|
|
143
|
+
* Get attachments by their IDs with associated blobs.
|
|
144
|
+
*
|
|
145
|
+
* @param attachmentIds - Array of attachment IDs to fetch.
|
|
146
|
+
*/
|
|
147
|
+
getAttachmentsByIds(attachmentIds: string[]): Promise<{
|
|
148
|
+
id: string;
|
|
149
|
+
createdAt: string;
|
|
150
|
+
name: string;
|
|
151
|
+
recordType: string;
|
|
152
|
+
recordId: string;
|
|
153
|
+
blobId: string;
|
|
154
|
+
blob: {
|
|
155
|
+
id: string;
|
|
156
|
+
key: string;
|
|
157
|
+
filename: string;
|
|
158
|
+
contentType: string | null;
|
|
159
|
+
metadata: unknown;
|
|
160
|
+
serviceName: string;
|
|
161
|
+
byteSize: number;
|
|
162
|
+
checksum: string | null;
|
|
163
|
+
createdAt: string;
|
|
164
|
+
} | null;
|
|
165
|
+
}[]>;
|
|
166
|
+
/**
|
|
167
|
+
* Delete a single attachment by ID.
|
|
168
|
+
*/
|
|
169
|
+
deleteAttachment(attachmentId: string): Promise<boolean>;
|
|
170
|
+
/**
|
|
171
|
+
* Delete attachments for a record (without deleting the blobs).
|
|
172
|
+
*/
|
|
173
|
+
deleteAttachments(recordType: string, recordId: string, name?: string): Promise<number>;
|
|
174
|
+
/**
|
|
175
|
+
* Get direct upload credentials (for S3). Also creates a pending blob record
|
|
176
|
+
* that will be finalized after upload.
|
|
177
|
+
*/
|
|
178
|
+
getDirectUploadUrl(options: {
|
|
179
|
+
contentType?: string;
|
|
180
|
+
expiresIn?: number;
|
|
181
|
+
filename: string;
|
|
182
|
+
metadata?: Record<string, any>;
|
|
183
|
+
serviceName?: TDiskNames;
|
|
184
|
+
}): Promise<{
|
|
185
|
+
blobId: string;
|
|
186
|
+
headers?: Record<string, string>;
|
|
187
|
+
key: string;
|
|
188
|
+
url: string;
|
|
189
|
+
} | null>;
|
|
190
|
+
/**
|
|
191
|
+
* Finalize a direct upload by updating blob metadata. Call this after the
|
|
192
|
+
* client has uploaded to S3.
|
|
193
|
+
*/
|
|
194
|
+
finalizeDirectUpload(blobId: string, actualSize: number): Promise<void>;
|
|
195
|
+
/**
|
|
196
|
+
* Update blob metadata (for automatic extraction).
|
|
197
|
+
*/
|
|
198
|
+
updateBlobMetadata(blobId: string, metadata: Record<string, any>): Promise<void>;
|
|
199
|
+
/**
|
|
200
|
+
* Get existing variant or return null.
|
|
201
|
+
*/
|
|
202
|
+
getVariant(blobId: string, transformations: ImageTransformations): Promise<StorageBlob | null>;
|
|
203
|
+
/**
|
|
204
|
+
* Create variant blob and record.
|
|
205
|
+
*/
|
|
206
|
+
createVariant(blobId: string, transformations: ImageTransformations, variantBuffer: Buffer): Promise<StorageBlob>;
|
|
207
|
+
/**
|
|
208
|
+
* Get blobs that have no attachments (orphaned). Useful for cleanup jobs.
|
|
209
|
+
*/
|
|
210
|
+
getUnattachedBlobs(options?: {
|
|
211
|
+
olderThan?: string;
|
|
212
|
+
limit?: number;
|
|
213
|
+
}): Promise<StorageBlob[]>;
|
|
214
|
+
/**
|
|
215
|
+
* Get pending blobs that were never finalized (stuck direct uploads).
|
|
216
|
+
*/
|
|
217
|
+
getPendingBlobs(olderThan?: string): Promise<StorageBlob[]>;
|
|
218
|
+
/**
|
|
219
|
+
* Purge unattached blobs.
|
|
220
|
+
*/
|
|
221
|
+
purgeUnattached(olderThan?: string): Promise<number>;
|
|
222
|
+
/**
|
|
223
|
+
* Create a signed, tamper-proof reference to a blob.
|
|
224
|
+
* Use findSigned() to resolve back to blob.
|
|
225
|
+
*
|
|
226
|
+
* Algorithm:
|
|
227
|
+
* 1. Create payload with blobId and expiration timestamp
|
|
228
|
+
* 2. JSON stringify and base64url encode the payload
|
|
229
|
+
* 3. Create HMAC-SHA256 signature of the encoded payload
|
|
230
|
+
* 4. Return payload.signature format
|
|
231
|
+
*/
|
|
232
|
+
signedId(blobId: string, expiresIn?: number): string;
|
|
233
|
+
/**
|
|
234
|
+
* Find blob by signed ID. Returns null if invalid or expired.
|
|
235
|
+
* Uses constant-time comparison to prevent timing attacks.
|
|
236
|
+
*
|
|
237
|
+
* Algorithm:
|
|
238
|
+
* 1. Split signed ID into payload and signature
|
|
239
|
+
* 2. Decode and recompute expected signature
|
|
240
|
+
* 3. Use timingSafeEqual for constant-time comparison
|
|
241
|
+
* 4. Check expiration timestamp
|
|
242
|
+
* 5. Return blob if valid, null otherwise
|
|
243
|
+
*/
|
|
244
|
+
findSigned(signedId: string): Promise<StorageBlob | null>;
|
|
245
|
+
/**
|
|
246
|
+
* Get a single attachment handle (one-to-one relationship).
|
|
247
|
+
* Similar to Rails' `has_one_attached :avatar`.
|
|
248
|
+
*
|
|
249
|
+
* Algorithm:
|
|
250
|
+
* Returns an object with methods to manage a single attachment:
|
|
251
|
+
* - attach(): Replace existing attachment with new blob
|
|
252
|
+
* - get(): Get attached blob or null
|
|
253
|
+
* - url(): Get signed URL
|
|
254
|
+
* - variant(): Get/generate image variant
|
|
255
|
+
* - purge(): Delete attachment and blob
|
|
256
|
+
*
|
|
257
|
+
* @template TTable - The table name (must be in TTableNames)
|
|
258
|
+
*/
|
|
259
|
+
one<TTable extends TTableNames>(recordType: TTable, recordId: string, name: AttachmentName<TAttachments, TTable>): {
|
|
260
|
+
/**
|
|
261
|
+
* Attach a blob to this record (replaces existing).
|
|
262
|
+
* Auto-triggers extractBlobMetadata workflow.
|
|
263
|
+
*/
|
|
264
|
+
attach(blobId: string): Promise<{
|
|
265
|
+
id: string;
|
|
266
|
+
createdAt: string;
|
|
267
|
+
name: string;
|
|
268
|
+
recordType: string;
|
|
269
|
+
recordId: string;
|
|
270
|
+
blobId: string;
|
|
271
|
+
}>;
|
|
272
|
+
/** Get the attached blob or null. */
|
|
273
|
+
get(): Promise<StorageBlob | null>;
|
|
274
|
+
/** Check if a blob is attached. */
|
|
275
|
+
attached(): Promise<boolean>;
|
|
276
|
+
/** Get signed URL or null. */
|
|
277
|
+
url(options?: {
|
|
278
|
+
expiresIn?: number;
|
|
279
|
+
disposition?: "inline" | "attachment";
|
|
280
|
+
}): Promise<string | null>;
|
|
281
|
+
/** Get permanent public URL or null. */
|
|
282
|
+
publicUrl(): Promise<string | null>;
|
|
283
|
+
/** Get blob metadata or null. */
|
|
284
|
+
metadata(): Promise<Record<string, unknown> | null>;
|
|
285
|
+
/** Check if metadata has been extracted. */
|
|
286
|
+
analyzed(): Promise<boolean>;
|
|
287
|
+
/** Check if blob supports preview/variant generation. */
|
|
288
|
+
representable(): Promise<boolean>;
|
|
289
|
+
/**
|
|
290
|
+
* Get variant URL. Auto-enqueues generation if not ready.
|
|
291
|
+
* Returns URL if variant exists, null if generating.
|
|
292
|
+
*/
|
|
293
|
+
variant(transformations: ImageTransformations, expiresIn?: number): Promise<string | null>;
|
|
294
|
+
/**
|
|
295
|
+
* Get preview URL. Auto-enqueues generation if not ready.
|
|
296
|
+
* Returns URL if preview exists, null if generating.
|
|
297
|
+
*/
|
|
298
|
+
preview(expiresIn?: number, timeInSeconds?: number): Promise<string | null>;
|
|
299
|
+
/** Detach blob from record (keeps blob for reuse). */
|
|
300
|
+
detach(): Promise<boolean>;
|
|
301
|
+
/** Delete attachment AND blob immediately. */
|
|
302
|
+
purge(): Promise<boolean>;
|
|
303
|
+
/** Enqueue attachment and blob deletion in background. */
|
|
304
|
+
purgeLater(): Promise<boolean>;
|
|
305
|
+
/** Download blob content as Buffer. */
|
|
306
|
+
download(): Promise<Buffer | null>;
|
|
307
|
+
/** Download to temp file, run callback, auto-cleanup. */
|
|
308
|
+
open<T>(callback: (filePath: string) => Promise<T> | T): Promise<T | null>;
|
|
309
|
+
/**
|
|
310
|
+
* Smart representation - variant for images, preview for videos/PDFs.
|
|
311
|
+
* Auto-enqueues generation if not ready.
|
|
312
|
+
*/
|
|
313
|
+
representation(transformations: ImageTransformations, expiresIn?: number): Promise<string | null>;
|
|
314
|
+
/** Get blob's byte size. */
|
|
315
|
+
byteSize(): Promise<number | null>;
|
|
316
|
+
/** Get blob's content type. */
|
|
317
|
+
contentType(): Promise<string | null>;
|
|
318
|
+
/** Get blob's filename. */
|
|
319
|
+
filename(): Promise<string | null>;
|
|
320
|
+
/** Get signed, tamper-proof ID for the blob. */
|
|
321
|
+
signedId(expiresIn?: number): Promise<string | null>;
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Get a multiple attachment handle (one-to-many relationship).
|
|
325
|
+
* Similar to Rails' `has_many_attached :images`.
|
|
326
|
+
*
|
|
327
|
+
* Algorithm:
|
|
328
|
+
* Returns an object with methods to manage multiple attachments:
|
|
329
|
+
* - attach(): Add blobs (doesn't replace existing)
|
|
330
|
+
* - list(): Get all attached blobs
|
|
331
|
+
* - urls(): Get signed URLs for all
|
|
332
|
+
* - variants(): Get/generate variants for all
|
|
333
|
+
* - purge(): Delete all or specific attachment
|
|
334
|
+
*
|
|
335
|
+
* @template TTable - The table name (must be in TTableNames)
|
|
336
|
+
*/
|
|
337
|
+
many<TTable extends TTableNames>(recordType: TTable, recordId: string, name: AttachmentName<TAttachments, TTable>): {
|
|
338
|
+
/**
|
|
339
|
+
* Attach one or more blobs (adds to existing, doesn't replace).
|
|
340
|
+
* Auto-triggers extractBlobMetadata for each blob.
|
|
341
|
+
*/
|
|
342
|
+
attach(blobIds: string | string[]): Promise<{
|
|
343
|
+
id: string;
|
|
344
|
+
createdAt: string;
|
|
345
|
+
name: string;
|
|
346
|
+
recordType: string;
|
|
347
|
+
recordId: string;
|
|
348
|
+
blobId: string;
|
|
349
|
+
}[]>;
|
|
350
|
+
/** Get all attached blobs. */
|
|
351
|
+
list(): Promise<StorageBlob[]>;
|
|
352
|
+
/** Count attached blobs. */
|
|
353
|
+
count(): Promise<number>;
|
|
354
|
+
/** Get signed URLs for all blobs. */
|
|
355
|
+
urls(options?: {
|
|
356
|
+
expiresIn?: number;
|
|
357
|
+
}): Promise<string[]>;
|
|
358
|
+
/** Get public URLs for all blobs. */
|
|
359
|
+
publicUrls(): Promise<(string | null)[]>;
|
|
360
|
+
/** Get metadata for all blobs. */
|
|
361
|
+
metadata(): Promise<(Record<string, unknown> | null)[]>;
|
|
362
|
+
/** Check which blobs have been analyzed. */
|
|
363
|
+
analyzed(): Promise<boolean[]>;
|
|
364
|
+
/** Check which blobs support preview/variants. */
|
|
365
|
+
representable(): Promise<boolean[]>;
|
|
366
|
+
/** Get variant URLs for all blobs. Auto-enqueues if not ready. */
|
|
367
|
+
variants(transformations: ImageTransformations, expiresIn?: number): Promise<(string | null)[]>;
|
|
368
|
+
/** Get preview URLs for all blobs. Auto-enqueues if not ready. */
|
|
369
|
+
previews(expiresIn?: number, timeInSeconds?: number): Promise<(string | null)[]>;
|
|
370
|
+
/** Detach specific blob or all blobs (keeps blobs for reuse). */
|
|
371
|
+
detach(blobId?: string): Promise<number>;
|
|
372
|
+
/** Delete specific or all attachments AND blobs immediately. */
|
|
373
|
+
purge(blobId?: string): Promise<number>;
|
|
374
|
+
/** Enqueue specific or all attachments for background deletion. */
|
|
375
|
+
purgeLater(blobId?: string): Promise<number>;
|
|
376
|
+
/** Download all blobs as Buffers. */
|
|
377
|
+
download(): Promise<Buffer[]>;
|
|
378
|
+
/** Download each blob to temp file, run callback for each. */
|
|
379
|
+
open<T>(callback: (filePath: string, blob: StorageBlob) => Promise<T> | T): Promise<T[]>;
|
|
380
|
+
/**
|
|
381
|
+
* Smart representations - variant for images, preview for videos/PDFs.
|
|
382
|
+
* Auto-enqueues generation if not ready.
|
|
383
|
+
*/
|
|
384
|
+
representations(transformations: ImageTransformations, expiresIn?: number): Promise<(string | null)[]>;
|
|
385
|
+
/** Get byte sizes for all blobs. */
|
|
386
|
+
byteSizes(): Promise<number[]>;
|
|
387
|
+
/** Get content types for all blobs. */
|
|
388
|
+
contentTypes(): Promise<(string | null)[]>;
|
|
389
|
+
/** Get filenames for all blobs. */
|
|
390
|
+
filenames(): Promise<string[]>;
|
|
391
|
+
/** Get signed IDs for all blobs. */
|
|
392
|
+
signedIds(expiresIn?: number): Promise<string[]>;
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
export { Storage };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{join as e}from"node:path";import"es-toolkit";function t(e){let t=null,n=null,r=null,i=null,a=async i=>{if(!t||!r)throw Error(`Workflow "${n}" not registered`);let a=r,o=a.workflowID;if(!o)throw Error(`DBOS.workflowID is not available in this context`);let s={container:t,workflowId:o,input:i,step:(e,t)=>a.runStep(t,{name:e})};return e.run(s)};return{inputSchema:e.input,get name(){return n},register(o,s,c){t=o,n=s,r=c,i=c.registerWorkflow(a,{name:s,...e.config})},async start(t){if(!i||!n||!r)throw Error(`Workflow not registered. Ensure the worker is started before triggering workflows.`);let a=e.input.parse(t),o=await r.startWorkflow(i)(a);return{workflowId:o.workflowID,getStatus:()=>o.getStatus(),getResult:()=>o.getResult()}}}}function n(e){let t=null,n=null,r=null,i=async(i,a)=>{if(!t||!r)throw Error(`Workflow "${n}" not registered`);let o=r,s=o.workflowID;if(!s)throw Error(`DBOS.workflowID is not available in this context`);let c={container:t,workflowId:s,scheduledTime:i,step:(e,t)=>o.runStep(t,{name:e})};return e.run(c)};return{crontab:e.crontab,get name(){return n},register(a,o,s){t=a,n=o,r=s,s.registerScheduled(s.registerWorkflow(i,{name:o}),{crontab:e.crontab})}}}export{n as defineScheduledWorkflow,t as defineWorkflow};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineWorkflow as e}from"../workflow.mjs";import{join as t}from"node:path";import{ALL_FORMATS as n,BlobSource as r,Input as i}from"mediabunny";import a from"sharp";import o from"zod";const s=e({input:o.object({blobId:o.string()}),async run({container:e,input:{blobId:o},step:s}){let c=await s(`fetch-blob`,async()=>e.storage.primary.getBlob(o));if(!c)throw Error(`Blob ${o} not found`);let l=await s(`download-blob`,async()=>e.storage.primary.downloadBlob(o));if(!l)throw Error(`Failed to download blob ${o}`);let u={};return c.contentType?.startsWith(`image/`)?u=await s(`extract-image-metadata`,async()=>{let e=await a(l).metadata();return{width:e.width,height:e.height,format:e.format,hasAlpha:e.hasAlpha,space:e.space}}):c.contentType?.startsWith(`video/`)||c.contentType?.startsWith(`audio/`)?u=await s(`extract-media-metadata`,async()=>{let e=new Uint8Array(l),t=new i({source:new r(new Blob([e],{type:c.contentType||`video/mp4`})),formats:n}),a=await t.computeDuration(),o=await t.getMetadataTags(),s={},u={},d=!1,f=!1;try{let e=await t.getPrimaryVideoTrack();if(e){d=!0;let t=e.displayWidth&&e.displayHeight?e.displayWidth/e.displayHeight:null;s={width:e.displayWidth,height:e.displayHeight,rotation:e.rotation,angle:e.rotation,displayAspectRatio:t}}}catch{}try{let e=await t.getPrimaryAudioTrack();e&&(f=!0,u={sampleRate:e.sampleRate,channels:e.numberOfChannels})}catch{}return{duration:a,video:d,audio:f,...s,...u,tags:o}}):c.contentType===`application/pdf`&&(u=await s(`extract-pdf-metadata`,async()=>{try{let e=await import(`pdfjs-dist/legacy/build/pdf.mjs`),n=`${t(process.cwd(),`node_modules/pdfjs-dist/standard_fonts`)}/`,r=await e.getDocument({data:new Uint8Array(l),standardFontDataUrl:n}).promise,i=await r.getMetadata(),a=(await r.getPage(1)).getViewport({scale:1}),o=i.info;return{pageCount:r.numPages,width:a.width,height:a.height,title:o?.Title||null,author:o?.Author||null,subject:o?.Subject||null,keywords:o?.Keywords||null,creator:o?.Creator||null,producer:o?.Producer||null,creationDate:o?.CreationDate||null,modificationDate:o?.ModDate||null,pdfVersion:o?.PDFFormatVersion||null}}catch(t){return e.logger.error({error:t,errorMessage:t instanceof Error?t.message:String(t),errorStack:t instanceof Error?t.stack:void 0,errorCode:t?.code,blobId:o},`Failed to extract PDF metadata`),{error:`Failed to extract PDF metadata`,errorMessage:t instanceof Error?t.message:String(t)}}})),await s(`save-metadata`,async()=>{await e.storage.primary.updateBlobMetadata(o,{...u,analyzed:!0})}),e.logger.info({blobId:o,metadata:u},`Metadata extracted`),{...u,analyzed:!0}}});export{s as extractBlobMetadata};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import "../workflow.mjs";
|
|
2
|
+
import "../container.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/api/workflows/generate-image-variant.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Image transformations schema.
|
|
9
|
+
* Supports resize, rotate, flip, flop, sharpen, blur, grayscale, format conversion.
|
|
10
|
+
*/
|
|
11
|
+
declare const transformationsSchema: z.ZodObject<{
|
|
12
|
+
resize: z.ZodOptional<z.ZodObject<{
|
|
13
|
+
width: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
height: z.ZodOptional<z.ZodNumber>;
|
|
15
|
+
fit: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
fill: "fill";
|
|
17
|
+
cover: "cover";
|
|
18
|
+
contain: "contain";
|
|
19
|
+
inside: "inside";
|
|
20
|
+
outside: "outside";
|
|
21
|
+
}>>;
|
|
22
|
+
position: z.ZodOptional<z.ZodEnum<{
|
|
23
|
+
left: "left";
|
|
24
|
+
right: "right";
|
|
25
|
+
top: "top";
|
|
26
|
+
"right top": "right top";
|
|
27
|
+
"right bottom": "right bottom";
|
|
28
|
+
bottom: "bottom";
|
|
29
|
+
"left bottom": "left bottom";
|
|
30
|
+
"left top": "left top";
|
|
31
|
+
centre: "centre";
|
|
32
|
+
}>>;
|
|
33
|
+
kernel: z.ZodOptional<z.ZodEnum<{
|
|
34
|
+
nearest: "nearest";
|
|
35
|
+
linear: "linear";
|
|
36
|
+
cubic: "cubic";
|
|
37
|
+
mitchell: "mitchell";
|
|
38
|
+
lanczos2: "lanczos2";
|
|
39
|
+
lanczos3: "lanczos3";
|
|
40
|
+
}>>;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
rotate: z.ZodOptional<z.ZodNumber>;
|
|
43
|
+
flip: z.ZodOptional<z.ZodBoolean>;
|
|
44
|
+
flop: z.ZodOptional<z.ZodBoolean>;
|
|
45
|
+
sharpen: z.ZodOptional<z.ZodBoolean>;
|
|
46
|
+
blur: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
grayscale: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
format: z.ZodOptional<z.ZodEnum<{
|
|
49
|
+
jpeg: "jpeg";
|
|
50
|
+
png: "png";
|
|
51
|
+
webp: "webp";
|
|
52
|
+
avif: "avif";
|
|
53
|
+
gif: "gif";
|
|
54
|
+
}>>;
|
|
55
|
+
quality: z.ZodOptional<z.ZodNumber>;
|
|
56
|
+
preview: z.ZodOptional<z.ZodLiteral<true>>;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
/**
|
|
59
|
+
* Types for image transformations.
|
|
60
|
+
*/
|
|
61
|
+
type ImageTransformations = z.infer<typeof transformationsSchema>;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { ImageTransformations, transformationsSchema };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineWorkflow as e}from"../workflow.mjs";import t from"sharp";import{z as n}from"zod";const r=n.object({width:n.number().optional(),height:n.number().optional(),fit:n.enum([`cover`,`contain`,`fill`,`inside`,`outside`]).optional(),position:n.enum([`top`,`right top`,`right`,`right bottom`,`bottom`,`left bottom`,`left`,`left top`,`centre`]).optional(),kernel:n.enum([`nearest`,`linear`,`cubic`,`mitchell`,`lanczos2`,`lanczos3`]).optional()}),i=n.object({resize:r.optional(),rotate:n.number().optional(),flip:n.boolean().optional(),flop:n.boolean().optional(),sharpen:n.boolean().optional(),blur:n.number().optional(),grayscale:n.boolean().optional(),format:n.enum([`jpeg`,`png`,`webp`,`avif`,`gif`]).optional(),quality:n.number().min(1).max(100).optional(),preview:n.literal(!0).optional()}),a=e({input:n.object({blobId:n.string(),transformations:i}),async run({container:e,input:{blobId:n,transformations:r},step:i}){if(!await i(`fetch-blob`,async()=>e.storage.primary.getBlob(n)))throw Error(`Blob ${n} not found`);let a=await i(`download-blob`,async()=>e.storage.primary.downloadBlob(n));if(!a)throw Error(`Failed to download blob ${n}`);let o=await i(`apply-transformations`,async()=>{let e=t(a);return r.resize&&(e=e.resize({width:r.resize.width,height:r.resize.height,fit:r.resize.fit,position:r.resize.position,kernel:r.resize.kernel})),r.rotate!==void 0&&(e=e.rotate(r.rotate)),r.flip&&(e=e.flip()),r.flop&&(e=e.flop()),r.sharpen&&(e=e.sharpen()),r.blur!==void 0&&(e=e.blur(r.blur)),r.grayscale&&(e=e.grayscale()),r.format&&(e=e.toFormat(r.format,{quality:r.quality})),e.toBuffer()}),s=await i(`store-variant`,async()=>e.storage.primary.createVariant(n,r,o));return e.logger.info({blobId:n,variantId:s.id},`Image variant generated`),s}});export{a as generateImageVariant};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineWorkflow as e}from"../workflow.mjs";import{join as t}from"node:path";import n from"sharp";import r from"zod";import{spawn as i}from"node:child_process";import{createCanvas as a}from"canvas";const o=e({input:r.object({blobId:r.string(),timeInSeconds:r.number().optional()}),async run({container:e,input:{blobId:r,timeInSeconds:o=1},step:s}){let c=await s(`fetch-blob`,async()=>e.storage.primary.getBlob(r));if(!c)throw Error(`Blob ${r} not found`);let l=await s(`download-blob`,async()=>e.storage.primary.downloadBlob(r));if(!l)throw Error(`Failed to download blob ${r}`);let u=null;if(c.contentType?.startsWith(`video/`))u=await s(`generate-video-preview`,async()=>new Promise((t,a)=>{try{let s=i(`ffmpeg`,[`-i`,`pipe:0`,`-ss`,o.toString(),`-frames:v`,`1`,`-f`,`image2pipe`,`-c:v`,`png`,`pipe:1`]),c=[],u=[];s.stdout.on(`data`,e=>{c.push(e)}),s.stderr.on(`data`,e=>{u.push(e)}),s.on(`close`,async i=>{if(i===0)try{t(await n(Buffer.concat(c)).jpeg({quality:80}).toBuffer())}catch(t){e.logger.error({error:t,blobId:r},`Failed to convert video frame to JPEG`),a(t)}else{let t=Buffer.concat(u).toString(),n=Error(`FFmpeg exited with code ${i}: ${t}`);e.logger.error({error:n,blobId:r,code:i,stderr:t},`Failed to generate video preview`),a(n)}}),s.on(`error`,t=>{e.logger.error({error:t,blobId:r},`Failed to spawn FFmpeg process`),a(t)}),s.stdin.on(`error`,t=>{t.code!==`EPIPE`&&e.logger.error({error:t,blobId:r},`Failed to write to FFmpeg stdin`)}),s.stdin.write(l),s.stdin.end()}catch(t){e.logger.error({error:t,blobId:r},`Failed to generate video preview`),a(t)}}));else if(c.contentType===`application/pdf`)u=await s(`generate-pdf-preview`,async()=>{try{let e=await import(`pdfjs-dist/legacy/build/pdf.mjs`),r=`${t(process.cwd(),`node_modules/pdfjs-dist/standard_fonts`)}/`,i=await(await e.getDocument({data:new Uint8Array(l),standardFontDataUrl:r}).promise).getPage(1),o=i.getViewport({scale:2}),s=a(o.width,o.height),c=s.getContext(`2d`);return await i.render({canvasContext:c,viewport:o,canvas:s}).promise,await n(s.toBuffer(`image/png`)).resize(800,800,{fit:`inside`,withoutEnlargement:!0}).jpeg({quality:85}).toBuffer()}catch(t){throw e.logger.error({error:t,errorMessage:t instanceof Error?t.message:String(t),errorStack:t instanceof Error?t.stack:void 0,errorCode:t?.code,blobId:r},`Failed to generate PDF preview`),t}});else if(c.contentType?.startsWith(`image/`))u=await s(`generate-image-preview`,async()=>await n(l).resize(800,800,{fit:`inside`,withoutEnlargement:!0}).jpeg({quality:85}).toBuffer());else throw Error(`Preview generation not supported for content type: ${c.contentType}`);let d=await s(`store-preview`,async()=>await e.storage.primary.createVariant(r,{preview:!0},u));return e.logger.info({blobId:r,previewId:d.id,contentType:c.contentType},`Preview generated`),d}});export{o as generatePreview};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineWorkflow as e}from"../workflow.mjs";import t from"zod";const n=e({input:t.object({attachmentIds:t.array(t.string()).min(1)}),async run({container:e,input:{attachmentIds:t},step:n}){let r=await n(`fetch-attachments`,async()=>(await e.storage.primary.getAttachmentsByIds(t)).filter(e=>e.blob!==null).map(e=>({attachmentId:e.id,blobId:e.blob.id})));return await n(`delete-attachments`,async()=>{for(let{attachmentId:t}of r)await e.storage.primary.deleteAttachment(t)}),await n(`delete-blobs`,async()=>{for(let{blobId:t}of r)await e.storage.primary.deleteBlob(t)}),e.logger.info({attachmentIds:t,blobCount:r.length},`Attachments and blobs purged`),{purgedCount:r.length}}});export{n as purgeAttachment};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineScheduledWorkflow as e}from"../workflow.mjs";import{defineAuthSchema as t}from"../auth-schema.mjs";import{lt as n}from"drizzle-orm";const r=t();function i(t=`0 0 * * *`){return e({crontab:t,async run({container:e,step:t,scheduledTime:i}){let a=e.auth.auditLog?.retentionDays??90,o=new Date(i);o.setDate(o.getDate()-a);let s=o.toISOString(),c=await t(`delete-old-logs`,async()=>{let{auditLogs:t}=r.tables;return(await e.db.primary.delete(t).where(n(t.createdAt,s)).returning({id:t.id})).length});e.logger.info({deletedCount:c,retentionDays:a,cutoffDate:s},`Audit log purge completed`)}})}export{i as definePurgeAuditLogs};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineScheduledWorkflow as e}from"../workflow.mjs";function t(t=`0 0 * * *`){return e({crontab:t,async run({container:e,step:t}){let n=new Date(Date.now()-2880*60*1e3).toISOString(),r=await t(`fetch-unattached-blobs`,async()=>e.storage.primary.getUnattachedBlobs({olderThan:n})),i=await t(`fetch-pending-blobs`,async()=>e.storage.primary.getPendingBlobs(n)),a=[...r,...i],o=0;for(let n of a)await t(`delete-blob`,async()=>{await e.storage.primary.deleteBlob(n.id),o++});e.logger.info({purgedCount:o,unattachedCount:r.length,pendingCount:i.length},`Orphaned blobs purged`)}})}export{t as definePurgeUnattachedBlobs};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{defineWorkflow as e}from"../workflow.mjs";import{defineAuthSchema as t}from"../auth-schema.mjs";import{dbChangesEvent as n}from"../event.mjs";import{z as r}from"zod";const i=t(),a=r.object({changes:r.array(r.object({_table:r.string(),old:r.record(r.string(),r.unknown()).nullable(),new:r.record(r.string(),r.unknown()).nullable()})),dbName:r.string(),organizationId:r.string().nullable(),requestId:r.string(),sessionId:r.string().nullable(),userId:r.string().nullable()});function o(e){return e.old===null?`INSERT`:e.new===null?`DELETE`:`UPDATE`}const s=e({input:a,async run({container:e,step:t,input:r}){let{dbName:a,changes:s,organizationId:c,userId:l,sessionId:u,requestId:d}=r;if(s.length===0)return{processed:0,audited:0,published:0};let f=new Date().toISOString(),p=0,m=0;for(let r of s){let s=r._table,h=o(r),g=`${a}.${s}`;e.auth.shouldAudit(g)&&(await t(`audit:${g}`,async()=>{await e.db.primary.insert(i.tables.auditLogs).values({tableName:g,action:h,oldData:r.old,newData:r.new,organizationId:c,userId:l,sessionId:u,requestId:d,createdAt:f})}),p++),await t(`event:${g}`,async()=>{await n.emit({action:h,oldData:r.old,newData:r.new,organizationId:c,tableName:g,timestamp:f,userId:l})}),m++}return{processed:s.length,audited:p,published:m}}});export{s as trackDbChanges};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Container } from "./api/container.mjs";
|
|
2
|
+
import { CommandContext } from "./context.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
//#region src/cli/command.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The command interface.
|
|
9
|
+
*/
|
|
10
|
+
interface Command<C extends Container, A = readonly unknown[], O = Record<string, unknown>> {
|
|
11
|
+
aliases: string[];
|
|
12
|
+
description: string;
|
|
13
|
+
args: z.ZodTuple<readonly [z.ZodTypeAny, ...z.ZodTypeAny[]], z.ZodTypeAny | null> | z.ZodArray<z.ZodTypeAny>;
|
|
14
|
+
opts: z.ZodObject<z.ZodRawShape>;
|
|
15
|
+
run: (ctx: CommandContext<C, A, O>) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Define a CLI command with type-safe arguments and options.
|
|
19
|
+
*
|
|
20
|
+
* The command name is automatically derived from the filename relative to the commands directory:
|
|
21
|
+
* - `<APPOS_DIR>/<COMMANDS_DIR>/db/push.ts` → `db:push`
|
|
22
|
+
* - `<APPOS_DIR>/<COMMANDS_DIR>/dev.ts` → `dev`
|
|
23
|
+
*
|
|
24
|
+
* Where:
|
|
25
|
+
* - `APPOS_DIR` defaults to "appos" (see constants.ts)
|
|
26
|
+
* - `COMMANDS_DIR` defaults to "commands" (see constants.ts)
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* // appos/commands/db/migrate.ts
|
|
31
|
+
* import { defineCommand } from "appos/cli";
|
|
32
|
+
* import { z } from "appos/zod";
|
|
33
|
+
*
|
|
34
|
+
* export default defineCommand({
|
|
35
|
+
* description: "Run database migrations",
|
|
36
|
+
* args: z.tuple([]),
|
|
37
|
+
* opts: z.object({
|
|
38
|
+
* dry: z.boolean().default(false)
|
|
39
|
+
* }),
|
|
40
|
+
* async run(args, opts, container) {
|
|
41
|
+
* await container.db.primary.migrate({ dryRun: opts.dry });
|
|
42
|
+
* }
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
declare function defineCommand<C extends Container = Container, ASchema extends z.ZodSchema<unknown> | undefined = undefined, OSchema extends z.ZodSchema<unknown> | undefined = undefined>(def: {
|
|
47
|
+
aliases?: string[];
|
|
48
|
+
description: string;
|
|
49
|
+
args: ASchema;
|
|
50
|
+
opts: OSchema;
|
|
51
|
+
run: (ctx: CommandContext<C, ASchema extends z.ZodSchema<infer A> ? A : readonly unknown[], OSchema extends z.ZodSchema<infer O> ? O : Record<string, unknown>>) => Promise<void>;
|
|
52
|
+
}): Command<C, ASchema extends z.ZodSchema<infer A> ? A : readonly unknown[], OSchema extends z.ZodSchema<infer O> ? O : Record<string, unknown>>;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { Command, defineCommand };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function e(e){return{aliases:e.aliases||[],description:e.description,args:e.args||[],opts:e.opts||{},run:e.run}}export{e as defineCommand};
|