@develit-io/backend-sdk 5.4.5

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/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright 2025 Develit.io s.r.o.
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
4
+
5
+ THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Develit Backend SDK
2
+
3
+ A TypeScript SDK for building Cloudflare Workers with database integration, command patterns, and automatic audit logging.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @develit-io/backend-sdk
9
+ ```
10
+
11
+ ## Core Features
12
+
13
+ - **Worker Framework**: Decorator-based actions with `@action()` and `@service()`
14
+ - **Database Integration**: Built-in Drizzle ORM with `initializeDB()`
15
+ - **Command Pattern**: Reusable, parameterized database operations
16
+ - **Atomic Audit Logging**: Audit logs included in same database batch
17
+ - **Type Safety**: Full TypeScript support throughout
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ @service('user-service')
23
+ export default class UserService extends develitWorker(WorkerEntrypoint<UserEnv>) {
24
+
25
+ constructor(env: Env, ctx: ExecutionContext) {
26
+ super(env, ctx)
27
+ // Setup audit logging
28
+ const auditWriter = createAuditLogWriter(tables.auditLog)
29
+ this.initializeDB(env.USER_DB, tables, auditWriter)
30
+ // Now available: this.db
31
+ }
32
+
33
+ @action('create-user')
34
+ async createUser(input: CreateUserInput): Promise<IRPCResponse<CreateUserOutput>> {
35
+ return this.handleAction(
36
+ {data: input, schema: createUserInputSchema},
37
+ {successMessage: 'User created successfully'},
38
+ () => {
39
+ await this.db.execute(createUserCommand(userData))
40
+ })
41
+ }
42
+ }
43
+ ```
44
+
45
+ ## Key Exports
46
+
47
+ ```typescript
48
+ // Core functionality
49
+ export { develitWorker, action, service } from '@develit-io/workers-sdk'
50
+ export { defineCommand, DatabaseTransaction, createAuditLogWriter } from '@develit-io/workers-sdk'
51
+
52
+ // Built-in worker properties after initializeDB():
53
+ // - this.db: DatabaseTransaction
54
+ ```
55
+
56
+ ## Examples
57
+
58
+ - **[defineCommand Usage](./examples/define-command.md)** - Creating parameterized commands
59
+ - **[DatabaseTransaction Usage](./examples/database-transaction.md)** - Transaction and audit logging
60
+
61
+ ## Requirements
62
+
63
+ - Node.js 18+
64
+ - TypeScript 5.0+
65
+ - Cloudflare Workers environment
66
+ - Drizzle ORM setup
67
+
68
+ ## License
69
+
70
+ ISC License - see package.json for details.
package/dist/index.cjs ADDED
@@ -0,0 +1,464 @@
1
+ 'use strict';
2
+
3
+ const drizzleOrm = require('drizzle-orm');
4
+ const pgCore = require('drizzle-orm/pg-core');
5
+ const sqliteCore = require('drizzle-orm/sqlite-core');
6
+ require('http-status-codes');
7
+ const z = require('zod/v4/core');
8
+ const h3 = require('h3');
9
+ const consola = require('consola');
10
+ const drizzleKit = require('drizzle-kit');
11
+ const superjson = require('superjson');
12
+
13
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
14
+
15
+ function _interopNamespaceCompat(e) {
16
+ if (e && typeof e === 'object' && 'default' in e) return e;
17
+ const n = Object.create(null);
18
+ if (e) {
19
+ for (const k in e) {
20
+ n[k] = e[k];
21
+ }
22
+ }
23
+ n.default = e;
24
+ return n;
25
+ }
26
+
27
+ const z__namespace = /*#__PURE__*/_interopNamespaceCompat(z);
28
+ const superjson__default = /*#__PURE__*/_interopDefaultCompat(superjson);
29
+
30
+ const base = {
31
+ id: sqliteCore.text("id").primaryKey(),
32
+ createdAt: sqliteCore.integer("created_at", { mode: "timestamp_ms" }).default(
33
+ drizzleOrm.sql`(unixepoch('subsec') * 1000)`
34
+ ),
35
+ modifiedAt: sqliteCore.integer("modified_at", { mode: "timestamp_ms" }).default(drizzleOrm.sql`null`).$onUpdate(() => /* @__PURE__ */ new Date())
36
+ };
37
+ const basePostgres = {
38
+ id: pgCore.uuid("id").primaryKey(),
39
+ createdAt: pgCore.timestamp("created_at", { mode: "date", withTimezone: false }).defaultNow().notNull(),
40
+ modifiedAt: pgCore.timestamp("modified_at", { mode: "date", withTimezone: false }).default(drizzleOrm.sql`null`).$onUpdate(() => /* @__PURE__ */ new Date())
41
+ };
42
+
43
+ const ibanZodSchema = new z__namespace.$ZodString({
44
+ type: "string",
45
+ checks: [
46
+ new z__namespace.$ZodCheckMinLength({
47
+ check: "min_length",
48
+ minimum: 1
49
+ }),
50
+ new z__namespace.$ZodCheckRegex({
51
+ check: "string_format",
52
+ format: "regex",
53
+ pattern: /^([A-Z]{2}[0-9]{2})([A-Z0-9]{4})([A-Z0-9]{4})([A-Z0-9]{4})([A-Z0-9]{4})?$/
54
+ })
55
+ ]
56
+ });
57
+ const swiftZodSchema = new z__namespace.$ZodString({
58
+ type: "string",
59
+ checks: [
60
+ new z__namespace.$ZodCheckMinLength({
61
+ check: "min_length",
62
+ minimum: 1
63
+ }),
64
+ new z__namespace.$ZodCheckRegex({
65
+ check: "string_format",
66
+ format: "regex",
67
+ pattern: /^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/
68
+ })
69
+ ]
70
+ });
71
+
72
+ const handleActionResponse = ({
73
+ error,
74
+ status,
75
+ message,
76
+ data
77
+ }) => {
78
+ if (error) {
79
+ throw h3.createError({ status, message });
80
+ }
81
+ if (data === void 0 || data === null) {
82
+ throw h3.createError({
83
+ statusCode: 500,
84
+ message: "Could not process the request. (ACTION_RESPONSE_FAILED)"
85
+ });
86
+ }
87
+ return data;
88
+ };
89
+
90
+ const createInternalError = (error, details) => {
91
+ return {
92
+ status: details?.status || 500,
93
+ code: details?.code || "UNKNOWN_ERROR",
94
+ message: details?.message || (error instanceof Error ? error.message : "An unexpected error occurred.")
95
+ };
96
+ };
97
+ const isInternalError = (error) => {
98
+ return typeof error === "object" && error !== null && "message" in error && "code" in error;
99
+ };
100
+
101
+ const RPCResponse = {
102
+ /**
103
+ * ✅ Constructs a successful RPC response.
104
+ *
105
+ * This method wraps the provided data in a standardized response format,
106
+ * ensuring consistency across API responses.
107
+ *
108
+ * @template T - The type of the response data.
109
+ * @param message - Response message
110
+ * @param detail - Optional -> Contains data and status code
111
+ * @returns An `IRPCResponse<T>` with the provided data and no error.
112
+ */
113
+ ok(message, detail) {
114
+ return {
115
+ status: detail?.status || 200,
116
+ data: detail?.data,
117
+ error: false,
118
+ message
119
+ };
120
+ },
121
+ /**
122
+ * ❌ Constructs a generic service error response.
123
+ *
124
+ * This method logs the error and returns a standardized error response.
125
+ *
126
+ * @template T - The expected response type (typically ignored in errors).
127
+ * @param error - An `RPCError` containing error details.
128
+ * @returns An `IRPCResponse<T>` with `null` data and the provided error.
129
+ */
130
+ serviceError(error) {
131
+ consola.consola.error(error.message);
132
+ return {
133
+ status: error.status,
134
+ message: error.message,
135
+ data: null,
136
+ error: true
137
+ };
138
+ },
139
+ /**
140
+ * ❌ Constructs a validation error response (HTTP 400).
141
+ *
142
+ * This is a convenience method for returning validation errors.
143
+ * It internally delegates to `serviceError()`.
144
+ *
145
+ * @template T - The expected response type (typically ignored in errors).
146
+ * @param error - An `RPCError` representing a validation failure.
147
+ * @returns An `IRPCResponse<T>` with `null` data and the provided error.
148
+ */
149
+ validationError(error) {
150
+ return RPCResponse.serviceError(error);
151
+ }
152
+ };
153
+
154
+ const useResult = async (promise) => {
155
+ try {
156
+ return [await promise, null];
157
+ } catch (error) {
158
+ return [null, isInternalError(error) ? error : createInternalError(error)];
159
+ }
160
+ };
161
+ const useResultSync = (fn) => {
162
+ try {
163
+ return [fn(), null];
164
+ } catch (error) {
165
+ return [null, isInternalError(error) ? error : createInternalError(error)];
166
+ }
167
+ };
168
+
169
+ async function handleAction(worker, input, options = {}, actionExecution) {
170
+ const {
171
+ successMessage = "Operation completed successfully.",
172
+ logOutputTransform,
173
+ logInputTransform,
174
+ skipValidation = false,
175
+ skipLogging = false
176
+ } = options;
177
+ try {
178
+ if (!skipLogging && input) {
179
+ const inputToLog = logInputTransform ? logInputTransform(input.data) : input;
180
+ worker.logInput(inputToLog);
181
+ }
182
+ let validatedInput = input?.data || null;
183
+ if (!skipValidation && input) {
184
+ validatedInput = worker.handleInput({
185
+ input: input.data,
186
+ schema: input.schema
187
+ });
188
+ }
189
+ const [data, error] = await useResult(
190
+ actionExecution(validatedInput)
191
+ );
192
+ if (error) {
193
+ worker.logError(error);
194
+ return RPCResponse.serviceError(error);
195
+ }
196
+ if (!skipLogging) {
197
+ const outputToLog = logOutputTransform ? logOutputTransform(data) : data;
198
+ worker.logOutput(outputToLog);
199
+ }
200
+ return RPCResponse.ok(successMessage, { data });
201
+ } catch (error) {
202
+ const internalError = createInternalError(error);
203
+ worker.logError(internalError);
204
+ return RPCResponse.serviceError(internalError);
205
+ }
206
+ }
207
+
208
+ const drizzleConfig = drizzleKit.defineConfig({
209
+ dialect: "sqlite",
210
+ schema: "./src/database/schema/",
211
+ out: "./src/database/migrations/"
212
+ });
213
+ function first(rows) {
214
+ return rows.length > 0 ? rows[0] : void 0;
215
+ }
216
+ function firstOrError(rows) {
217
+ if (rows.length === 0) {
218
+ throw new Error("Query did not return any data.");
219
+ }
220
+ return rows[0];
221
+ }
222
+ const uuidv4 = () => crypto.randomUUID();
223
+
224
+ const calculateExponentialBackoff = (attempts, baseDelaySeconds) => {
225
+ return baseDelaySeconds ** attempts;
226
+ };
227
+
228
+ const asNonEmpty = (arr) => {
229
+ return arr.length > 0 ? [arr[0], ...arr.slice(1)] : null;
230
+ };
231
+
232
+ class DatabaseTransaction {
233
+ constructor(db, serviceName, auditLogWriter) {
234
+ this.db = db;
235
+ this.serviceName = serviceName;
236
+ this.auditLogWriter = auditLogWriter;
237
+ }
238
+ commands = [];
239
+ logs = [];
240
+ ids = [];
241
+ enqueue(commands) {
242
+ return (Array.isArray(commands) ? commands : [commands]).map((cmd) => {
243
+ const commandItem = cmd.handler(this.db);
244
+ const auditLogPayload = {
245
+ ...commandItem.logPayload,
246
+ service: this.serviceName || "unknown-service"
247
+ };
248
+ this.commands.push(commandItem.command);
249
+ this.logs.push(auditLogPayload);
250
+ if (commandItem.id) {
251
+ this.ids.push(commandItem.id);
252
+ }
253
+ return commandItem;
254
+ }).reduce((acc, curr) => {
255
+ acc.push(curr.id);
256
+ return acc;
257
+ }, []);
258
+ }
259
+ async execute(commandItem) {
260
+ this.enqueue(commandItem);
261
+ await this.executeAll();
262
+ }
263
+ async executeAll() {
264
+ let allCommands = [...this.commands];
265
+ if (this.auditLogWriter && this.logs.length > 0) {
266
+ const auditCommands = this.auditLogWriter(this.logs, this.db);
267
+ allCommands.push(...auditCommands);
268
+ }
269
+ const batchItems = asNonEmpty(allCommands);
270
+ if (batchItems) {
271
+ await this.db.batch(batchItems);
272
+ }
273
+ this.commands = [];
274
+ this.logs = [];
275
+ this.ids = [];
276
+ }
277
+ getLogs() {
278
+ return [...this.logs];
279
+ }
280
+ getIds() {
281
+ return [...this.ids];
282
+ }
283
+ getCommandsCount() {
284
+ return this.commands.length;
285
+ }
286
+ }
287
+
288
+ const defineCommand = (handler) => {
289
+ return (params) => ({
290
+ handler: (db) => handler(db, params)
291
+ });
292
+ };
293
+
294
+ function createAuditLogWriter(table) {
295
+ return (logs, db) => {
296
+ if (logs.length === 0) return [];
297
+ const auditRecords = logs.map((log) => ({
298
+ ...log,
299
+ createdAt: /* @__PURE__ */ new Date(),
300
+ id: crypto.randomUUID()
301
+ }));
302
+ return [db.insert(table).values(auditRecords)];
303
+ };
304
+ }
305
+
306
+ const service = (serviceName) => {
307
+ return function(constructor) {
308
+ return class extends constructor {
309
+ name = `${serviceName}-service`;
310
+ };
311
+ };
312
+ };
313
+
314
+ const action = (name) => {
315
+ return (_target, _propertyKey, descriptor) => {
316
+ const originalMethod = descriptor.value;
317
+ descriptor.value = function(...args) {
318
+ this.action = name;
319
+ return originalMethod.apply(this, args);
320
+ };
321
+ return descriptor;
322
+ };
323
+ };
324
+
325
+ function cloudflareQueue(options) {
326
+ return (_target, _propertyKey, descriptor) => {
327
+ const originalMethod = descriptor.value;
328
+ descriptor.value = async function(...args) {
329
+ const batch = args[0];
330
+ let retriedCount = 0;
331
+ batch.messages.forEach((msg) => {
332
+ const originalRetry = msg.retry?.bind(msg);
333
+ msg.retry = () => {
334
+ retriedCount++;
335
+ return originalRetry({
336
+ delaySeconds: calculateExponentialBackoff(
337
+ msg.attempts,
338
+ options.baseDelay
339
+ )
340
+ });
341
+ };
342
+ });
343
+ const result = await originalMethod.apply(this, args);
344
+ if (typeof this.logQueueRetries === "function") {
345
+ this.logQueueRetries({
346
+ message: `Retried ${retriedCount} out of ${batch.messages.length} messages`
347
+ });
348
+ }
349
+ return result;
350
+ };
351
+ };
352
+ }
353
+
354
+ function develitWorker(Worker) {
355
+ class DevelitWorker extends Worker {
356
+ name = "not-set";
357
+ action = "not-set";
358
+ // public db!: DatabaseTransaction<string>
359
+ async fetch() {
360
+ return new Response("Service is up and running!");
361
+ }
362
+ // initializeDB<TAuditAction = string>(
363
+ // database: D1Database,
364
+ // schema: Record<string, unknown>,
365
+ // auditLogWriter?: AuditLogWriter<TAuditAction>,
366
+ // ): void {
367
+ // const drizzleInstance = drizzle(database, {
368
+ // schema,
369
+ // }) as DrizzleD1Database<Record<string, unknown>>
370
+ // this.db = new DatabaseTransaction<TAuditAction>(
371
+ // drizzleInstance,
372
+ // this.name,
373
+ // auditLogWriter,
374
+ // ) as unknown as DatabaseTransaction<string>
375
+ // }
376
+ handleInput({
377
+ input,
378
+ schema
379
+ }) {
380
+ this.logInput({ input });
381
+ const parseResult = z__namespace.safeParse(schema, input);
382
+ if (!parseResult.success) {
383
+ const parseError = {
384
+ status: 400,
385
+ code: "INVALID_ACTION_INPUT",
386
+ message: z__namespace.prettifyError(parseResult.error)
387
+ };
388
+ this.logError(parseError);
389
+ throw RPCResponse.validationError(parseError);
390
+ }
391
+ if (!parseResult.data) {
392
+ throw createInternalError({
393
+ message: "The provided input could not be processed."
394
+ });
395
+ }
396
+ return parseResult.data;
397
+ }
398
+ log(data, identifier) {
399
+ const name = identifier ?? `${this.name}:${this.action}:log`;
400
+ console.log(name, {
401
+ entrypoint: this.name,
402
+ action: this.action,
403
+ identifier: name,
404
+ data: superjson__default.stringify(data)
405
+ });
406
+ }
407
+ logQueuePush(data) {
408
+ this.log(data, `${this.name}:${this.action}:queue-push`);
409
+ }
410
+ logQueuePull(data) {
411
+ this.log(data, `${this.name}:${this.action}:queue-pull`);
412
+ }
413
+ logQueueRetries(data) {
414
+ this.log(data, `${this.name}:${this.action}:queue-retries`);
415
+ }
416
+ logInput(data) {
417
+ this.log(data, `${this.name}:${this.action}:input`);
418
+ }
419
+ logOutput(data) {
420
+ this.log(data, `${this.name}:${this.action}:output`);
421
+ }
422
+ logError(error) {
423
+ this.log(error, `${this.name}:${this.action}:error`);
424
+ }
425
+ pushToQueue(queue, message) {
426
+ if (!Array.isArray(message))
427
+ return queue.send(message, { contentType: "v8" });
428
+ return queue.sendBatch(
429
+ message.map((m) => ({
430
+ body: m,
431
+ contentType: "v8"
432
+ }))
433
+ );
434
+ }
435
+ async handleAction(input, options = {}, actionExecution) {
436
+ return handleAction(this, input, options, actionExecution);
437
+ }
438
+ }
439
+ return DevelitWorker;
440
+ }
441
+
442
+ exports.DatabaseTransaction = DatabaseTransaction;
443
+ exports.RPCResponse = RPCResponse;
444
+ exports.action = action;
445
+ exports.base = base;
446
+ exports.basePostgres = basePostgres;
447
+ exports.calculateExponentialBackoff = calculateExponentialBackoff;
448
+ exports.cloudflareQueue = cloudflareQueue;
449
+ exports.createAuditLogWriter = createAuditLogWriter;
450
+ exports.createInternalError = createInternalError;
451
+ exports.defineCommand = defineCommand;
452
+ exports.develitWorker = develitWorker;
453
+ exports.drizzleConfig = drizzleConfig;
454
+ exports.first = first;
455
+ exports.firstOrError = firstOrError;
456
+ exports.handleAction = handleAction;
457
+ exports.handleActionResponse = handleActionResponse;
458
+ exports.ibanZodSchema = ibanZodSchema;
459
+ exports.isInternalError = isInternalError;
460
+ exports.service = service;
461
+ exports.swiftZodSchema = swiftZodSchema;
462
+ exports.useResult = useResult;
463
+ exports.useResultSync = useResultSync;
464
+ exports.uuidv4 = uuidv4;