appos 0.3.2-0 → 0.3.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-CcqAJY9P.mjs +2 -0
- package/dist/bin/better-sqlite3-CuQ3hsWl.mjs +2 -0
- package/dist/bin/bun-sql-DGeo-s_M.mjs +2 -0
- package/dist/bin/cache-3oO07miM.mjs +2 -0
- package/dist/bin/chunk-l9p7A9gZ.mjs +2 -0
- package/dist/bin/cockroach-BaICwY7N.mjs +2 -0
- package/dist/bin/database-CaysWPpa.mjs +2 -0
- package/dist/bin/esm-BvsccvmM.mjs +2 -0
- package/dist/bin/esm-CGKzJ7Am.mjs +3 -0
- package/dist/bin/event-DnSe3eh0.mjs +8 -0
- package/dist/bin/extract-blob-metadata-iqwTl2ft.mjs +170 -0
- package/dist/bin/generate-image-variant-Lyx0vhM6.mjs +2 -0
- package/dist/bin/generate-preview-0MrKxslA.mjs +2 -0
- package/dist/bin/libsql-DQJrZsU9.mjs +2 -0
- package/dist/bin/logger-BAGZLUzj.mjs +2 -0
- package/dist/bin/main.mjs +1201 -190
- package/dist/bin/migrator-B7iNKM8N.mjs +2 -0
- package/dist/bin/migrator-BKE1cSQQ.mjs +2 -0
- package/dist/bin/migrator-BXcbc9zs.mjs +2 -0
- package/dist/bin/migrator-B_XhRWZC.mjs +8 -0
- package/dist/bin/migrator-Bz52Gtr8.mjs +2 -0
- package/dist/bin/migrator-C7W-cZHB.mjs +2 -0
- package/dist/bin/migrator-CEnKyGSW.mjs +2 -0
- package/dist/bin/migrator-CHzIIl5X.mjs +2 -0
- package/dist/bin/migrator-CR-rjZdM.mjs +2 -0
- package/dist/bin/migrator-CjIr1ZCx.mjs +8 -0
- package/dist/bin/migrator-Cuubh2dg.mjs +2 -0
- package/dist/bin/migrator-D8m-ORbr.mjs +8 -0
- package/dist/bin/migrator-DBFwrhZH.mjs +2 -0
- package/dist/bin/migrator-DLmhW9u_.mjs +2 -0
- package/dist/bin/migrator-DLoHx807.mjs +4 -0
- package/dist/bin/migrator-DtN_iS87.mjs +2 -0
- package/dist/bin/migrator-Yc57lb3w.mjs +2 -0
- package/dist/bin/migrator-cEVXH3xC.mjs +2 -0
- package/dist/bin/migrator-hWi-sYIq.mjs +2 -0
- package/dist/bin/mysql2-DufFWkj4.mjs +2 -0
- package/dist/bin/neon-serverless-5a4h2VFz.mjs +2 -0
- package/dist/bin/node-CiOp4xrR.mjs +22 -0
- package/dist/bin/node-mssql-DvZGaUkB.mjs +322 -0
- package/dist/bin/node-postgres-BqbJVBQY.mjs +2 -0
- package/dist/bin/node-postgres-DnhRTTO8.mjs +2 -0
- package/dist/bin/open-0ksnL0S8.mjs +2 -0
- package/dist/bin/pdf-sUYeFPr4.mjs +14 -0
- package/dist/bin/pg-CaH8ptj-.mjs +2 -0
- package/dist/bin/pg-core-BLTZt9AH.mjs +8 -0
- package/dist/bin/pg-core-CGzidKaA.mjs +2 -0
- package/dist/bin/pglite-BJB9z7Ju.mjs +2 -0
- package/dist/bin/planetscale-serverless-H3RfLlMK.mjs +13 -0
- package/dist/bin/postgres-js-DuOf1eWm.mjs +2 -0
- package/dist/bin/purge-attachment-DQXpTtTx.mjs +2 -0
- package/dist/bin/purge-audit-logs-BEt2J2gD.mjs +2 -0
- package/dist/bin/{purge-unattached-blobs-Duvv8Izd.mjs → purge-unattached-blobs-DOmk4ddJ.mjs} +1 -1
- package/dist/bin/query-builder-DSRrR6X_.mjs +8 -0
- package/dist/bin/query-builder-V8-LDhvA.mjs +3 -0
- package/dist/bin/session-CdB1A-LB.mjs +14 -0
- package/dist/bin/session-Cl2e-_i8.mjs +8 -0
- package/dist/bin/singlestore-COft6TlR.mjs +8 -0
- package/dist/bin/sql-D-eKV1Dn.mjs +2 -0
- package/dist/bin/sqlite-cloud-Co9jOn5G.mjs +2 -0
- package/dist/bin/sqlite-proxy-Cpu78gJF.mjs +2 -0
- package/dist/bin/src-C-oXmCzx.mjs +6 -0
- package/dist/bin/table-3zUpWkMg.mjs +2 -0
- package/dist/bin/track-db-changes-DWyY5jXm.mjs +2 -0
- package/dist/bin/utils-CyoeCJlf.mjs +2 -0
- package/dist/bin/utils-EoqYQKy1.mjs +2 -0
- package/dist/bin/utils-bsypyqPl.mjs +2 -0
- package/dist/bin/vercel-postgres-HWL6xtqi.mjs +2 -0
- package/dist/bin/workflow-zxHDyfLq.mjs +2 -0
- package/dist/bin/youch-handler-DrYdbUhe.mjs +2 -0
- package/dist/bin/zod-MJjkEkRY.mjs +24 -0
- package/dist/exports/api/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/api/app-context.mjs +24 -1
- package/dist/exports/api/auth-schema.mjs +373 -1
- package/dist/exports/api/auth.d.mts +4 -0
- package/dist/exports/api/auth.mjs +188 -1
- package/dist/exports/api/cache.d.mts +2 -2
- package/dist/exports/api/cache.mjs +28 -1
- package/dist/exports/api/config.mjs +72 -1
- package/dist/exports/api/constants.mjs +92 -1
- package/dist/exports/api/container.mjs +49 -1
- package/dist/exports/api/database.mjs +218 -1
- package/dist/exports/api/event.mjs +236 -1
- package/dist/exports/api/i18n.mjs +45 -1
- package/dist/exports/api/index.mjs +20 -1
- package/dist/exports/api/instrumentation.mjs +40 -1
- package/dist/exports/api/logger.mjs +26 -1
- package/dist/exports/api/mailer.mjs +37 -1
- package/dist/exports/api/middleware.mjs +73 -1
- package/dist/exports/api/openapi.mjs +507 -1
- package/dist/exports/api/orm.mjs +43 -1
- package/dist/exports/api/otel.mjs +56 -1
- package/dist/exports/api/redis.mjs +41 -1
- package/dist/exports/api/storage-schema.mjs +72 -1
- package/dist/exports/api/storage.mjs +833 -1
- package/dist/exports/api/web/auth.mjs +17 -1
- package/dist/exports/api/workflow.mjs +196 -1
- package/dist/exports/api/workflows/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/api/workflows/api/auth-schema.mjs +373 -1
- package/dist/exports/api/workflows/api/auth.d.mts +4 -0
- package/dist/exports/api/workflows/api/cache.d.mts +2 -2
- package/dist/exports/api/workflows/api/event.mjs +126 -1
- package/dist/exports/api/workflows/api/redis.mjs +3 -1
- package/dist/exports/api/workflows/api/workflow.mjs +135 -1
- package/dist/exports/api/workflows/constants.mjs +23 -1
- package/dist/exports/api/workflows/extract-blob-metadata.mjs +132 -1
- package/dist/exports/api/workflows/generate-image-variant.d.mts +2 -2
- package/dist/exports/api/workflows/generate-image-variant.mjs +118 -1
- package/dist/exports/api/workflows/generate-preview.mjs +160 -1
- package/dist/exports/api/workflows/index.mjs +3 -1
- package/dist/exports/api/workflows/purge-attachment.mjs +34 -1
- package/dist/exports/api/workflows/purge-audit-logs.mjs +47 -1
- package/dist/exports/api/workflows/purge-unattached-blobs.mjs +46 -1
- package/dist/exports/api/workflows/track-db-changes.mjs +110 -1
- package/dist/exports/cli/_virtual/rolldown_runtime.mjs +36 -1
- package/dist/exports/cli/api/auth-schema.mjs +373 -1
- package/dist/exports/cli/api/auth.d.mts +4 -0
- package/dist/exports/cli/api/cache.d.mts +2 -2
- package/dist/exports/cli/api/event.mjs +126 -1
- package/dist/exports/cli/api/redis.mjs +3 -1
- package/dist/exports/cli/api/workflow.mjs +135 -1
- package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +132 -1
- package/dist/exports/cli/api/workflows/generate-image-variant.mjs +118 -1
- package/dist/exports/cli/api/workflows/generate-preview.mjs +160 -1
- package/dist/exports/cli/api/workflows/purge-attachment.mjs +34 -1
- package/dist/exports/cli/api/workflows/purge-audit-logs.mjs +47 -1
- package/dist/exports/cli/api/workflows/purge-unattached-blobs.mjs +46 -1
- package/dist/exports/cli/api/workflows/track-db-changes.mjs +110 -1
- package/dist/exports/cli/command.d.mts +2 -0
- package/dist/exports/cli/command.mjs +43 -1
- package/dist/exports/cli/constants.mjs +23 -1
- package/dist/exports/cli/index.mjs +3 -1
- package/dist/exports/devtools/index.js +4 -1
- package/dist/exports/tests/api/auth.d.mts +4 -0
- package/dist/exports/tests/api/cache.d.mts +2 -2
- package/dist/exports/tests/api/middleware/i18n.mjs +1 -1
- package/dist/exports/tests/api/middleware/youch-handler.mjs +1 -1
- package/dist/exports/tests/api/openapi.mjs +1 -1
- package/dist/exports/tests/api/server.mjs +1 -1
- package/dist/exports/tests/constants.mjs +1 -1
- package/dist/exports/vendors/date.js +1 -1
- package/dist/exports/vendors/toolkit.js +1 -1
- package/dist/exports/vendors/zod.js +1 -1
- package/dist/exports/vitest/globals.mjs +1 -1
- package/dist/exports/web/auth.js +75 -1
- package/dist/exports/web/i18n.js +45 -1
- package/dist/exports/web/index.js +8 -1
- package/package.json +19 -17
- package/dist/bin/auth-schema-Va0CYicu.mjs +0 -2
- package/dist/bin/event-8JibGFH_.mjs +0 -2
- package/dist/bin/extract-blob-metadata-DjPfHtQ2.mjs +0 -2
- package/dist/bin/generate-image-variant-D5VDFyWj.mjs +0 -2
- package/dist/bin/generate-preview-Dssw7w5U.mjs +0 -2
- package/dist/bin/purge-attachment-BBPzIxwt.mjs +0 -2
- package/dist/bin/purge-audit-logs-BeZy3IFM.mjs +0 -2
- package/dist/bin/track-db-changes-CFykw_YO.mjs +0 -2
- package/dist/bin/workflow-BNUZrj4F.mjs +0 -2
- package/dist/bin/youch-handler-BadUgHb0.mjs +0 -2
|
@@ -1 +1,126 @@
|
|
|
1
|
-
import"./redis.mjs";
|
|
1
|
+
import "./redis.mjs";
|
|
2
|
+
import "../constants.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import "es-toolkit";
|
|
6
|
+
|
|
7
|
+
//#region src/api/event.ts
|
|
8
|
+
/**
|
|
9
|
+
* Defines a type-safe event with in-memory and Redis pub/sub support.
|
|
10
|
+
*
|
|
11
|
+
* Algorithm:
|
|
12
|
+
* 1. Define event with input schema and in-memory run handler
|
|
13
|
+
* 2. On emit(): validate input, run in-memory handler, publish to Redis (fire-and-forget)
|
|
14
|
+
* 3. .subscribe() creates Redis subscription for tRPC/SSE/WebSocket handlers
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // api/events/order-status.ts
|
|
19
|
+
* export default defineEvent({
|
|
20
|
+
* input: z.object({
|
|
21
|
+
* orderId: z.string(),
|
|
22
|
+
* status: z.enum(["pending", "shipped", "delivered"]),
|
|
23
|
+
* }),
|
|
24
|
+
* async run(ctx) {
|
|
25
|
+
* ctx.container.logger.info(`Order ${ctx.input.orderId} is ${ctx.input.status}`);
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* // Emit from anywhere
|
|
30
|
+
* await orderStatus.emit({ orderId: "123", status: "shipped" });
|
|
31
|
+
*
|
|
32
|
+
* // Subscribe (e.g., in tRPC router)
|
|
33
|
+
* const unsubscribe = await orderStatus.subscribe(async (ctx) => {
|
|
34
|
+
* // Push to client via SSE/WebSocket
|
|
35
|
+
* });
|
|
36
|
+
* // Cleanup when client disconnects
|
|
37
|
+
* unsubscribe();
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
function defineEvent(options) {
|
|
41
|
+
let container = null;
|
|
42
|
+
let eventName = null;
|
|
43
|
+
return {
|
|
44
|
+
inputSchema: options.input,
|
|
45
|
+
get name() {
|
|
46
|
+
return eventName;
|
|
47
|
+
},
|
|
48
|
+
register(c, name) {
|
|
49
|
+
container = c;
|
|
50
|
+
eventName = name;
|
|
51
|
+
},
|
|
52
|
+
async emit(input) {
|
|
53
|
+
if (!container || !eventName) throw new Error("Event not registered. Ensure the worker is started before emitting events.");
|
|
54
|
+
const validated = options.input.parse(input);
|
|
55
|
+
const ctx = {
|
|
56
|
+
container,
|
|
57
|
+
input: validated
|
|
58
|
+
};
|
|
59
|
+
await options.run(ctx);
|
|
60
|
+
container.eventBus.publish(eventName, validated).catch((err) => {
|
|
61
|
+
container.logger.error({
|
|
62
|
+
err,
|
|
63
|
+
event: eventName
|
|
64
|
+
}, "Redis publish failed");
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
async subscribe(handler) {
|
|
68
|
+
if (!container || !eventName) throw new Error("Event not registered. Ensure the worker is started before subscribing.");
|
|
69
|
+
return container.eventBus.subscribe(eventName, async (message) => {
|
|
70
|
+
const validated = options.input.parse(message);
|
|
71
|
+
const ctx = {
|
|
72
|
+
container,
|
|
73
|
+
input: validated
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
await handler(ctx);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
container.logger.error({
|
|
79
|
+
err,
|
|
80
|
+
event: eventName
|
|
81
|
+
}, "Event subscription handler error");
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Input schema for dbChangesEvent.
|
|
89
|
+
*/
|
|
90
|
+
const dbChangeInputSchema = z.object({
|
|
91
|
+
action: z.enum([
|
|
92
|
+
"INSERT",
|
|
93
|
+
"UPDATE",
|
|
94
|
+
"DELETE"
|
|
95
|
+
]),
|
|
96
|
+
newData: z.record(z.string(), z.unknown()).nullable(),
|
|
97
|
+
oldData: z.record(z.string(), z.unknown()).nullable(),
|
|
98
|
+
organizationId: z.string().nullable(),
|
|
99
|
+
tableName: z.string(),
|
|
100
|
+
timestamp: z.string(),
|
|
101
|
+
userId: z.string().nullable()
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Built-in event for database changes.
|
|
105
|
+
* Emitted by trackDbChanges workflow.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // Subscribe to DB changes (e.g., in tRPC subscription)
|
|
110
|
+
* import { dbChangesEvent } from "appos/api";
|
|
111
|
+
*
|
|
112
|
+
* const unsubscribe = await dbChangesEvent.subscribe(async (ctx) => {
|
|
113
|
+
* wsServer.publish("db-changes", JSON.stringify(ctx.input));
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* // Cleanup when done
|
|
117
|
+
* unsubscribe();
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
const dbChangesEvent = defineEvent({
|
|
121
|
+
input: dbChangeInputSchema,
|
|
122
|
+
async run() {}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
126
|
+
export { dbChangesEvent };
|
|
@@ -1 +1,135 @@
|
|
|
1
|
-
import"../constants.mjs";
|
|
1
|
+
import "../constants.mjs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import "es-toolkit";
|
|
4
|
+
|
|
5
|
+
//#region src/api/workflow.ts
|
|
6
|
+
/**
|
|
7
|
+
* Defines a durable workflow that can be triggered with type-safe input.
|
|
8
|
+
*
|
|
9
|
+
* Workflows are:
|
|
10
|
+
* - Durable: Automatically resume after crashes or restarts
|
|
11
|
+
* - Type-safe: Input and output types are inferred from the schema
|
|
12
|
+
* - Container-aware: Access to db, mailer, logger via ctx.container
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // api/workflows/send-welcome-email.ts
|
|
17
|
+
* export default defineWorkflow({
|
|
18
|
+
* input: z.object({
|
|
19
|
+
* userId: z.string(),
|
|
20
|
+
* email: z.string().email(),
|
|
21
|
+
* }),
|
|
22
|
+
* async run(ctx) {
|
|
23
|
+
* const user = await ctx.container.db.primary.query.users.findFirst({
|
|
24
|
+
* where: eq(users.id, ctx.input.userId),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* await ctx.container.mailer.send({
|
|
28
|
+
* to: ctx.input.email,
|
|
29
|
+
* subject: "Welcome!",
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* return { sent: true };
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Triggering from a route:
|
|
37
|
+
* import sendWelcomeEmail from "#api/workflows/send-welcome-email.ts";
|
|
38
|
+
* const handle = await sendWelcomeEmail.start({ userId: "123", email: "..." });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
function defineWorkflow(options) {
|
|
42
|
+
let container = null;
|
|
43
|
+
let workflowName = null;
|
|
44
|
+
let dbosInstance = null;
|
|
45
|
+
let registeredFn = null;
|
|
46
|
+
const workflowFn = async (input) => {
|
|
47
|
+
if (!container || !dbosInstance) throw new Error(`Workflow "${workflowName}" not registered`);
|
|
48
|
+
const dbos = dbosInstance;
|
|
49
|
+
const workflowId = dbos.workflowID;
|
|
50
|
+
if (!workflowId) throw new Error("DBOS.workflowID is not available in this context");
|
|
51
|
+
const ctx = {
|
|
52
|
+
container,
|
|
53
|
+
workflowId,
|
|
54
|
+
input,
|
|
55
|
+
step: (name, fn) => dbos.runStep(fn, { name })
|
|
56
|
+
};
|
|
57
|
+
return options.run(ctx);
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
inputSchema: options.input,
|
|
61
|
+
get name() {
|
|
62
|
+
return workflowName;
|
|
63
|
+
},
|
|
64
|
+
register(c, name, dbos) {
|
|
65
|
+
container = c;
|
|
66
|
+
workflowName = name;
|
|
67
|
+
dbosInstance = dbos;
|
|
68
|
+
registeredFn = dbos.registerWorkflow(workflowFn, {
|
|
69
|
+
name,
|
|
70
|
+
...options.config
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
async start(input) {
|
|
74
|
+
if (!registeredFn || !workflowName || !dbosInstance) throw new Error("Workflow not registered. Ensure the worker is started before triggering workflows.");
|
|
75
|
+
const validated = options.input.parse(input);
|
|
76
|
+
const handle = await dbosInstance.startWorkflow(registeredFn)(validated);
|
|
77
|
+
return {
|
|
78
|
+
workflowId: handle.workflowID,
|
|
79
|
+
getStatus: () => handle.getStatus(),
|
|
80
|
+
getResult: () => handle.getResult()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Defines a scheduled workflow that runs on a cron schedule.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* // api/workflows/daily-report.ts
|
|
91
|
+
* export default defineScheduledWorkflow({
|
|
92
|
+
* crontab: "0 9 * * *", // 9am daily
|
|
93
|
+
* async run(ctx) {
|
|
94
|
+
* const stats = await ctx.container.db.primary.query.stats.findMany();
|
|
95
|
+
* await ctx.container.mailer.send({
|
|
96
|
+
* to: "team@company.com",
|
|
97
|
+
* subject: `Daily Report - ${ctx.scheduledTime.toDateString()}`,
|
|
98
|
+
* });
|
|
99
|
+
* },
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
function defineScheduledWorkflow(options) {
|
|
104
|
+
let container = null;
|
|
105
|
+
let workflowName = null;
|
|
106
|
+
let dbosInstance = null;
|
|
107
|
+
const workflowFn = async (scheduledTime, _startTime) => {
|
|
108
|
+
if (!container || !dbosInstance) throw new Error(`Workflow "${workflowName}" not registered`);
|
|
109
|
+
const dbos = dbosInstance;
|
|
110
|
+
const workflowId = dbos.workflowID;
|
|
111
|
+
if (!workflowId) throw new Error("DBOS.workflowID is not available in this context");
|
|
112
|
+
const ctx = {
|
|
113
|
+
container,
|
|
114
|
+
workflowId,
|
|
115
|
+
scheduledTime,
|
|
116
|
+
step: (name, fn) => dbos.runStep(fn, { name })
|
|
117
|
+
};
|
|
118
|
+
return options.run(ctx);
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
crontab: options.crontab,
|
|
122
|
+
get name() {
|
|
123
|
+
return workflowName;
|
|
124
|
+
},
|
|
125
|
+
register(c, name, dbos) {
|
|
126
|
+
container = c;
|
|
127
|
+
workflowName = name;
|
|
128
|
+
dbosInstance = dbos;
|
|
129
|
+
dbos.registerScheduled(dbos.registerWorkflow(workflowFn, { name }), { crontab: options.crontab });
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { defineScheduledWorkflow, defineWorkflow };
|
|
@@ -1 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
//#region src/constants.ts
|
|
2
|
+
/**
|
|
3
|
+
* Directory for public static assets.
|
|
4
|
+
*
|
|
5
|
+
* Expected structure:
|
|
6
|
+
* - `<project-root>/public/` - Public directory root
|
|
7
|
+
* - `<project-root>/public/locales/` - i18n translation files
|
|
8
|
+
*
|
|
9
|
+
* @default "public"
|
|
10
|
+
*/
|
|
11
|
+
const PUBLIC_DIR = process.env.NODE_ENV === "production" ? "client" : "public";
|
|
12
|
+
/**
|
|
13
|
+
* File extension for code files based on environment.
|
|
14
|
+
*
|
|
15
|
+
* In development: `.ts` (TypeScript source files)
|
|
16
|
+
* In production: `.js` (bundled JavaScript files)
|
|
17
|
+
*
|
|
18
|
+
* @default "ts" in development, "js" in production
|
|
19
|
+
*/
|
|
20
|
+
const FILE_EXT = process.env.NODE_ENV === "production" ? "js" : "ts";
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { };
|
|
@@ -1 +1,132 @@
|
|
|
1
|
-
import{defineWorkflow
|
|
1
|
+
import { defineWorkflow } from "./api/workflow.mjs";
|
|
2
|
+
import z$1 from "zod";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ALL_FORMATS, BlobSource, Input } from "mediabunny";
|
|
5
|
+
|
|
6
|
+
//#region src/api/workflows/extract-blob-metadata.ts
|
|
7
|
+
const extractBlobMetadata = defineWorkflow({
|
|
8
|
+
input: z$1.object({ blobId: z$1.string() }),
|
|
9
|
+
async run({ container, input: { blobId }, step }) {
|
|
10
|
+
const blob = await step("fetch-blob", async () => {
|
|
11
|
+
return container.storage.primary.getBlob(blobId);
|
|
12
|
+
});
|
|
13
|
+
if (!blob) throw new Error(`Blob ${blobId} not found`);
|
|
14
|
+
const buffer = await step("download-blob", async () => {
|
|
15
|
+
return container.storage.primary.downloadBlob(blobId);
|
|
16
|
+
});
|
|
17
|
+
if (!buffer) throw new Error(`Failed to download blob ${blobId}`);
|
|
18
|
+
let metadata = {};
|
|
19
|
+
if (blob.contentType?.startsWith("image/")) metadata = await step("extract-image-metadata", async () => {
|
|
20
|
+
const sharp = (await import("sharp")).default;
|
|
21
|
+
const info = await sharp(buffer).metadata();
|
|
22
|
+
return {
|
|
23
|
+
width: info.width,
|
|
24
|
+
height: info.height,
|
|
25
|
+
format: info.format,
|
|
26
|
+
hasAlpha: info.hasAlpha,
|
|
27
|
+
space: info.space
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
else if (blob.contentType?.startsWith("video/") || blob.contentType?.startsWith("audio/")) metadata = await step("extract-media-metadata", async () => {
|
|
31
|
+
const uint8Array = new Uint8Array(buffer);
|
|
32
|
+
const input = new Input({
|
|
33
|
+
source: new BlobSource(new Blob([uint8Array], { type: blob.contentType || "video/mp4" })),
|
|
34
|
+
formats: ALL_FORMATS
|
|
35
|
+
});
|
|
36
|
+
const duration = await input.computeDuration();
|
|
37
|
+
const tags = await input.getMetadataTags();
|
|
38
|
+
let videoData = {};
|
|
39
|
+
let audioData = {};
|
|
40
|
+
let hasVideo = false;
|
|
41
|
+
let hasAudio = false;
|
|
42
|
+
try {
|
|
43
|
+
const videoTrack = await input.getPrimaryVideoTrack();
|
|
44
|
+
if (videoTrack) {
|
|
45
|
+
hasVideo = true;
|
|
46
|
+
const displayAspectRatio = videoTrack.displayWidth && videoTrack.displayHeight ? videoTrack.displayWidth / videoTrack.displayHeight : null;
|
|
47
|
+
videoData = {
|
|
48
|
+
width: videoTrack.displayWidth,
|
|
49
|
+
height: videoTrack.displayHeight,
|
|
50
|
+
rotation: videoTrack.rotation,
|
|
51
|
+
angle: videoTrack.rotation,
|
|
52
|
+
displayAspectRatio
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
try {
|
|
57
|
+
const audioTrack = await input.getPrimaryAudioTrack();
|
|
58
|
+
if (audioTrack) {
|
|
59
|
+
hasAudio = true;
|
|
60
|
+
audioData = {
|
|
61
|
+
sampleRate: audioTrack.sampleRate,
|
|
62
|
+
channels: audioTrack.numberOfChannels
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return {
|
|
67
|
+
duration,
|
|
68
|
+
video: hasVideo,
|
|
69
|
+
audio: hasAudio,
|
|
70
|
+
...videoData,
|
|
71
|
+
...audioData,
|
|
72
|
+
tags
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
else if (blob.contentType === "application/pdf") metadata = await step("extract-pdf-metadata", async () => {
|
|
76
|
+
try {
|
|
77
|
+
const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
78
|
+
const standardFontDataUrl = `${join(process.cwd(), "node_modules/pdfjs-dist/standard_fonts")}/`;
|
|
79
|
+
const pdf = await pdfjsLib.getDocument({
|
|
80
|
+
data: new Uint8Array(buffer),
|
|
81
|
+
standardFontDataUrl
|
|
82
|
+
}).promise;
|
|
83
|
+
const pdfMetadata = await pdf.getMetadata();
|
|
84
|
+
const viewport = (await pdf.getPage(1)).getViewport({ scale: 1 });
|
|
85
|
+
const info = pdfMetadata.info;
|
|
86
|
+
return {
|
|
87
|
+
pageCount: pdf.numPages,
|
|
88
|
+
width: viewport.width,
|
|
89
|
+
height: viewport.height,
|
|
90
|
+
title: info?.Title || null,
|
|
91
|
+
author: info?.Author || null,
|
|
92
|
+
subject: info?.Subject || null,
|
|
93
|
+
keywords: info?.Keywords || null,
|
|
94
|
+
creator: info?.Creator || null,
|
|
95
|
+
producer: info?.Producer || null,
|
|
96
|
+
creationDate: info?.CreationDate || null,
|
|
97
|
+
modificationDate: info?.ModDate || null,
|
|
98
|
+
pdfVersion: info?.PDFFormatVersion || null
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
container.logger.error({
|
|
102
|
+
error,
|
|
103
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
104
|
+
errorStack: error instanceof Error ? error.stack : void 0,
|
|
105
|
+
errorCode: error?.code,
|
|
106
|
+
blobId
|
|
107
|
+
}, "Failed to extract PDF metadata");
|
|
108
|
+
return {
|
|
109
|
+
error: "Failed to extract PDF metadata",
|
|
110
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
await step("save-metadata", async () => {
|
|
115
|
+
await container.storage.primary.updateBlobMetadata(blobId, {
|
|
116
|
+
...metadata,
|
|
117
|
+
analyzed: true
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
container.logger.info({
|
|
121
|
+
blobId,
|
|
122
|
+
metadata
|
|
123
|
+
}, "Metadata extracted");
|
|
124
|
+
return {
|
|
125
|
+
...metadata,
|
|
126
|
+
analyzed: true
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
export { extractBlobMetadata };
|
|
@@ -1 +1,118 @@
|
|
|
1
|
-
import{defineWorkflow
|
|
1
|
+
import { defineWorkflow } from "./api/workflow.mjs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/api/workflows/generate-image-variant.ts
|
|
5
|
+
/**
|
|
6
|
+
* Resize options schema for image transformations.
|
|
7
|
+
*/
|
|
8
|
+
const resizeSchema = z.object({
|
|
9
|
+
width: z.number().optional(),
|
|
10
|
+
height: z.number().optional(),
|
|
11
|
+
fit: z.enum([
|
|
12
|
+
"cover",
|
|
13
|
+
"contain",
|
|
14
|
+
"fill",
|
|
15
|
+
"inside",
|
|
16
|
+
"outside"
|
|
17
|
+
]).optional(),
|
|
18
|
+
position: z.enum([
|
|
19
|
+
"top",
|
|
20
|
+
"right top",
|
|
21
|
+
"right",
|
|
22
|
+
"right bottom",
|
|
23
|
+
"bottom",
|
|
24
|
+
"left bottom",
|
|
25
|
+
"left",
|
|
26
|
+
"left top",
|
|
27
|
+
"centre"
|
|
28
|
+
]).optional(),
|
|
29
|
+
kernel: z.enum([
|
|
30
|
+
"nearest",
|
|
31
|
+
"linear",
|
|
32
|
+
"cubic",
|
|
33
|
+
"mitchell",
|
|
34
|
+
"lanczos2",
|
|
35
|
+
"lanczos3"
|
|
36
|
+
]).optional()
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* Image transformations schema.
|
|
40
|
+
* Supports resize, rotate, flip, flop, sharpen, blur, grayscale, format conversion.
|
|
41
|
+
*/
|
|
42
|
+
const transformationsSchema = z.object({
|
|
43
|
+
resize: resizeSchema.optional(),
|
|
44
|
+
rotate: z.number().optional(),
|
|
45
|
+
flip: z.boolean().optional(),
|
|
46
|
+
flop: z.boolean().optional(),
|
|
47
|
+
sharpen: z.boolean().optional(),
|
|
48
|
+
blur: z.number().optional(),
|
|
49
|
+
grayscale: z.boolean().optional(),
|
|
50
|
+
format: z.enum([
|
|
51
|
+
"jpeg",
|
|
52
|
+
"png",
|
|
53
|
+
"webp",
|
|
54
|
+
"avif",
|
|
55
|
+
"gif"
|
|
56
|
+
]).optional(),
|
|
57
|
+
quality: z.number().min(1).max(100).optional(),
|
|
58
|
+
preview: z.literal(true).optional()
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Generate image variant workflow. Applies transformations to create variants.
|
|
62
|
+
*
|
|
63
|
+
* Algorithm:
|
|
64
|
+
* 1. Fetch blob by ID
|
|
65
|
+
* 2. Download blob content
|
|
66
|
+
* 3. Apply transformations using Sharp:
|
|
67
|
+
* - Resize with various fit options
|
|
68
|
+
* - Rotate by degrees
|
|
69
|
+
* - Flip/flop (vertical/horizontal mirror)
|
|
70
|
+
* - Sharpen, blur, grayscale filters
|
|
71
|
+
* - Format conversion with quality settings
|
|
72
|
+
* 4. Store variant with transformation metadata
|
|
73
|
+
*/
|
|
74
|
+
const generateImageVariant = defineWorkflow({
|
|
75
|
+
input: z.object({
|
|
76
|
+
blobId: z.string(),
|
|
77
|
+
transformations: transformationsSchema
|
|
78
|
+
}),
|
|
79
|
+
async run({ container, input: { blobId, transformations }, step }) {
|
|
80
|
+
if (!await step("fetch-blob", async () => {
|
|
81
|
+
return container.storage.primary.getBlob(blobId);
|
|
82
|
+
})) throw new Error(`Blob ${blobId} not found`);
|
|
83
|
+
const buffer = await step("download-blob", async () => {
|
|
84
|
+
return container.storage.primary.downloadBlob(blobId);
|
|
85
|
+
});
|
|
86
|
+
if (!buffer) throw new Error(`Failed to download blob ${blobId}`);
|
|
87
|
+
const variantBuffer = await step("apply-transformations", async () => {
|
|
88
|
+
const sharp = (await import("sharp")).default;
|
|
89
|
+
let pipeline = sharp(buffer);
|
|
90
|
+
if (transformations.resize) pipeline = pipeline.resize({
|
|
91
|
+
width: transformations.resize.width,
|
|
92
|
+
height: transformations.resize.height,
|
|
93
|
+
fit: transformations.resize.fit,
|
|
94
|
+
position: transformations.resize.position,
|
|
95
|
+
kernel: transformations.resize.kernel
|
|
96
|
+
});
|
|
97
|
+
if (transformations.rotate !== void 0) pipeline = pipeline.rotate(transformations.rotate);
|
|
98
|
+
if (transformations.flip) pipeline = pipeline.flip();
|
|
99
|
+
if (transformations.flop) pipeline = pipeline.flop();
|
|
100
|
+
if (transformations.sharpen) pipeline = pipeline.sharpen();
|
|
101
|
+
if (transformations.blur !== void 0) pipeline = pipeline.blur(transformations.blur);
|
|
102
|
+
if (transformations.grayscale) pipeline = pipeline.grayscale();
|
|
103
|
+
if (transformations.format) pipeline = pipeline.toFormat(transformations.format, { quality: transformations.quality });
|
|
104
|
+
return pipeline.toBuffer();
|
|
105
|
+
});
|
|
106
|
+
const variant = await step("store-variant", async () => {
|
|
107
|
+
return container.storage.primary.createVariant(blobId, transformations, variantBuffer);
|
|
108
|
+
});
|
|
109
|
+
container.logger.info({
|
|
110
|
+
blobId,
|
|
111
|
+
variantId: variant.id
|
|
112
|
+
}, "Image variant generated");
|
|
113
|
+
return variant;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { generateImageVariant };
|