@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 +5 -0
- package/README.md +70 -0
- package/dist/index.cjs +464 -0
- package/dist/index.d.cts +330 -0
- package/dist/index.d.mts +330 -0
- package/dist/index.d.ts +330 -0
- package/dist/index.mjs +423 -0
- package/package.json +39 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { timestamp, uuid } from 'drizzle-orm/pg-core';
|
|
3
|
+
import { integer, text } from 'drizzle-orm/sqlite-core';
|
|
4
|
+
import 'http-status-codes';
|
|
5
|
+
import * as z from 'zod/v4/core';
|
|
6
|
+
import { createError } from 'h3';
|
|
7
|
+
import { consola } from 'consola';
|
|
8
|
+
import { defineConfig } from 'drizzle-kit';
|
|
9
|
+
import superjson from 'superjson';
|
|
10
|
+
|
|
11
|
+
const base = {
|
|
12
|
+
id: text("id").primaryKey(),
|
|
13
|
+
createdAt: integer("created_at", { mode: "timestamp_ms" }).default(
|
|
14
|
+
sql`(unixepoch('subsec') * 1000)`
|
|
15
|
+
),
|
|
16
|
+
modifiedAt: integer("modified_at", { mode: "timestamp_ms" }).default(sql`null`).$onUpdate(() => /* @__PURE__ */ new Date())
|
|
17
|
+
};
|
|
18
|
+
const basePostgres = {
|
|
19
|
+
id: uuid("id").primaryKey(),
|
|
20
|
+
createdAt: timestamp("created_at", { mode: "date", withTimezone: false }).defaultNow().notNull(),
|
|
21
|
+
modifiedAt: timestamp("modified_at", { mode: "date", withTimezone: false }).default(sql`null`).$onUpdate(() => /* @__PURE__ */ new Date())
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ibanZodSchema = new z.$ZodString({
|
|
25
|
+
type: "string",
|
|
26
|
+
checks: [
|
|
27
|
+
new z.$ZodCheckMinLength({
|
|
28
|
+
check: "min_length",
|
|
29
|
+
minimum: 1
|
|
30
|
+
}),
|
|
31
|
+
new z.$ZodCheckRegex({
|
|
32
|
+
check: "string_format",
|
|
33
|
+
format: "regex",
|
|
34
|
+
pattern: /^([A-Z]{2}[0-9]{2})([A-Z0-9]{4})([A-Z0-9]{4})([A-Z0-9]{4})([A-Z0-9]{4})?$/
|
|
35
|
+
})
|
|
36
|
+
]
|
|
37
|
+
});
|
|
38
|
+
const swiftZodSchema = new z.$ZodString({
|
|
39
|
+
type: "string",
|
|
40
|
+
checks: [
|
|
41
|
+
new z.$ZodCheckMinLength({
|
|
42
|
+
check: "min_length",
|
|
43
|
+
minimum: 1
|
|
44
|
+
}),
|
|
45
|
+
new z.$ZodCheckRegex({
|
|
46
|
+
check: "string_format",
|
|
47
|
+
format: "regex",
|
|
48
|
+
pattern: /^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const handleActionResponse = ({
|
|
54
|
+
error,
|
|
55
|
+
status,
|
|
56
|
+
message,
|
|
57
|
+
data
|
|
58
|
+
}) => {
|
|
59
|
+
if (error) {
|
|
60
|
+
throw createError({ status, message });
|
|
61
|
+
}
|
|
62
|
+
if (data === void 0 || data === null) {
|
|
63
|
+
throw createError({
|
|
64
|
+
statusCode: 500,
|
|
65
|
+
message: "Could not process the request. (ACTION_RESPONSE_FAILED)"
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return data;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const createInternalError = (error, details) => {
|
|
72
|
+
return {
|
|
73
|
+
status: details?.status || 500,
|
|
74
|
+
code: details?.code || "UNKNOWN_ERROR",
|
|
75
|
+
message: details?.message || (error instanceof Error ? error.message : "An unexpected error occurred.")
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const isInternalError = (error) => {
|
|
79
|
+
return typeof error === "object" && error !== null && "message" in error && "code" in error;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const RPCResponse = {
|
|
83
|
+
/**
|
|
84
|
+
* ✅ Constructs a successful RPC response.
|
|
85
|
+
*
|
|
86
|
+
* This method wraps the provided data in a standardized response format,
|
|
87
|
+
* ensuring consistency across API responses.
|
|
88
|
+
*
|
|
89
|
+
* @template T - The type of the response data.
|
|
90
|
+
* @param message - Response message
|
|
91
|
+
* @param detail - Optional -> Contains data and status code
|
|
92
|
+
* @returns An `IRPCResponse<T>` with the provided data and no error.
|
|
93
|
+
*/
|
|
94
|
+
ok(message, detail) {
|
|
95
|
+
return {
|
|
96
|
+
status: detail?.status || 200,
|
|
97
|
+
data: detail?.data,
|
|
98
|
+
error: false,
|
|
99
|
+
message
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* ❌ Constructs a generic service error response.
|
|
104
|
+
*
|
|
105
|
+
* This method logs the error and returns a standardized error response.
|
|
106
|
+
*
|
|
107
|
+
* @template T - The expected response type (typically ignored in errors).
|
|
108
|
+
* @param error - An `RPCError` containing error details.
|
|
109
|
+
* @returns An `IRPCResponse<T>` with `null` data and the provided error.
|
|
110
|
+
*/
|
|
111
|
+
serviceError(error) {
|
|
112
|
+
consola.error(error.message);
|
|
113
|
+
return {
|
|
114
|
+
status: error.status,
|
|
115
|
+
message: error.message,
|
|
116
|
+
data: null,
|
|
117
|
+
error: true
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
/**
|
|
121
|
+
* ❌ Constructs a validation error response (HTTP 400).
|
|
122
|
+
*
|
|
123
|
+
* This is a convenience method for returning validation errors.
|
|
124
|
+
* It internally delegates to `serviceError()`.
|
|
125
|
+
*
|
|
126
|
+
* @template T - The expected response type (typically ignored in errors).
|
|
127
|
+
* @param error - An `RPCError` representing a validation failure.
|
|
128
|
+
* @returns An `IRPCResponse<T>` with `null` data and the provided error.
|
|
129
|
+
*/
|
|
130
|
+
validationError(error) {
|
|
131
|
+
return RPCResponse.serviceError(error);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const useResult = async (promise) => {
|
|
136
|
+
try {
|
|
137
|
+
return [await promise, null];
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return [null, isInternalError(error) ? error : createInternalError(error)];
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const useResultSync = (fn) => {
|
|
143
|
+
try {
|
|
144
|
+
return [fn(), null];
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return [null, isInternalError(error) ? error : createInternalError(error)];
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
async function handleAction(worker, input, options = {}, actionExecution) {
|
|
151
|
+
const {
|
|
152
|
+
successMessage = "Operation completed successfully.",
|
|
153
|
+
logOutputTransform,
|
|
154
|
+
logInputTransform,
|
|
155
|
+
skipValidation = false,
|
|
156
|
+
skipLogging = false
|
|
157
|
+
} = options;
|
|
158
|
+
try {
|
|
159
|
+
if (!skipLogging && input) {
|
|
160
|
+
const inputToLog = logInputTransform ? logInputTransform(input.data) : input;
|
|
161
|
+
worker.logInput(inputToLog);
|
|
162
|
+
}
|
|
163
|
+
let validatedInput = input?.data || null;
|
|
164
|
+
if (!skipValidation && input) {
|
|
165
|
+
validatedInput = worker.handleInput({
|
|
166
|
+
input: input.data,
|
|
167
|
+
schema: input.schema
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
const [data, error] = await useResult(
|
|
171
|
+
actionExecution(validatedInput)
|
|
172
|
+
);
|
|
173
|
+
if (error) {
|
|
174
|
+
worker.logError(error);
|
|
175
|
+
return RPCResponse.serviceError(error);
|
|
176
|
+
}
|
|
177
|
+
if (!skipLogging) {
|
|
178
|
+
const outputToLog = logOutputTransform ? logOutputTransform(data) : data;
|
|
179
|
+
worker.logOutput(outputToLog);
|
|
180
|
+
}
|
|
181
|
+
return RPCResponse.ok(successMessage, { data });
|
|
182
|
+
} catch (error) {
|
|
183
|
+
const internalError = createInternalError(error);
|
|
184
|
+
worker.logError(internalError);
|
|
185
|
+
return RPCResponse.serviceError(internalError);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const drizzleConfig = defineConfig({
|
|
190
|
+
dialect: "sqlite",
|
|
191
|
+
schema: "./src/database/schema/",
|
|
192
|
+
out: "./src/database/migrations/"
|
|
193
|
+
});
|
|
194
|
+
function first(rows) {
|
|
195
|
+
return rows.length > 0 ? rows[0] : void 0;
|
|
196
|
+
}
|
|
197
|
+
function firstOrError(rows) {
|
|
198
|
+
if (rows.length === 0) {
|
|
199
|
+
throw new Error("Query did not return any data.");
|
|
200
|
+
}
|
|
201
|
+
return rows[0];
|
|
202
|
+
}
|
|
203
|
+
const uuidv4 = () => crypto.randomUUID();
|
|
204
|
+
|
|
205
|
+
const calculateExponentialBackoff = (attempts, baseDelaySeconds) => {
|
|
206
|
+
return baseDelaySeconds ** attempts;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const asNonEmpty = (arr) => {
|
|
210
|
+
return arr.length > 0 ? [arr[0], ...arr.slice(1)] : null;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
class DatabaseTransaction {
|
|
214
|
+
constructor(db, serviceName, auditLogWriter) {
|
|
215
|
+
this.db = db;
|
|
216
|
+
this.serviceName = serviceName;
|
|
217
|
+
this.auditLogWriter = auditLogWriter;
|
|
218
|
+
}
|
|
219
|
+
commands = [];
|
|
220
|
+
logs = [];
|
|
221
|
+
ids = [];
|
|
222
|
+
enqueue(commands) {
|
|
223
|
+
return (Array.isArray(commands) ? commands : [commands]).map((cmd) => {
|
|
224
|
+
const commandItem = cmd.handler(this.db);
|
|
225
|
+
const auditLogPayload = {
|
|
226
|
+
...commandItem.logPayload,
|
|
227
|
+
service: this.serviceName || "unknown-service"
|
|
228
|
+
};
|
|
229
|
+
this.commands.push(commandItem.command);
|
|
230
|
+
this.logs.push(auditLogPayload);
|
|
231
|
+
if (commandItem.id) {
|
|
232
|
+
this.ids.push(commandItem.id);
|
|
233
|
+
}
|
|
234
|
+
return commandItem;
|
|
235
|
+
}).reduce((acc, curr) => {
|
|
236
|
+
acc.push(curr.id);
|
|
237
|
+
return acc;
|
|
238
|
+
}, []);
|
|
239
|
+
}
|
|
240
|
+
async execute(commandItem) {
|
|
241
|
+
this.enqueue(commandItem);
|
|
242
|
+
await this.executeAll();
|
|
243
|
+
}
|
|
244
|
+
async executeAll() {
|
|
245
|
+
let allCommands = [...this.commands];
|
|
246
|
+
if (this.auditLogWriter && this.logs.length > 0) {
|
|
247
|
+
const auditCommands = this.auditLogWriter(this.logs, this.db);
|
|
248
|
+
allCommands.push(...auditCommands);
|
|
249
|
+
}
|
|
250
|
+
const batchItems = asNonEmpty(allCommands);
|
|
251
|
+
if (batchItems) {
|
|
252
|
+
await this.db.batch(batchItems);
|
|
253
|
+
}
|
|
254
|
+
this.commands = [];
|
|
255
|
+
this.logs = [];
|
|
256
|
+
this.ids = [];
|
|
257
|
+
}
|
|
258
|
+
getLogs() {
|
|
259
|
+
return [...this.logs];
|
|
260
|
+
}
|
|
261
|
+
getIds() {
|
|
262
|
+
return [...this.ids];
|
|
263
|
+
}
|
|
264
|
+
getCommandsCount() {
|
|
265
|
+
return this.commands.length;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const defineCommand = (handler) => {
|
|
270
|
+
return (params) => ({
|
|
271
|
+
handler: (db) => handler(db, params)
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
function createAuditLogWriter(table) {
|
|
276
|
+
return (logs, db) => {
|
|
277
|
+
if (logs.length === 0) return [];
|
|
278
|
+
const auditRecords = logs.map((log) => ({
|
|
279
|
+
...log,
|
|
280
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
281
|
+
id: crypto.randomUUID()
|
|
282
|
+
}));
|
|
283
|
+
return [db.insert(table).values(auditRecords)];
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const service = (serviceName) => {
|
|
288
|
+
return function(constructor) {
|
|
289
|
+
return class extends constructor {
|
|
290
|
+
name = `${serviceName}-service`;
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const action = (name) => {
|
|
296
|
+
return (_target, _propertyKey, descriptor) => {
|
|
297
|
+
const originalMethod = descriptor.value;
|
|
298
|
+
descriptor.value = function(...args) {
|
|
299
|
+
this.action = name;
|
|
300
|
+
return originalMethod.apply(this, args);
|
|
301
|
+
};
|
|
302
|
+
return descriptor;
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
function cloudflareQueue(options) {
|
|
307
|
+
return (_target, _propertyKey, descriptor) => {
|
|
308
|
+
const originalMethod = descriptor.value;
|
|
309
|
+
descriptor.value = async function(...args) {
|
|
310
|
+
const batch = args[0];
|
|
311
|
+
let retriedCount = 0;
|
|
312
|
+
batch.messages.forEach((msg) => {
|
|
313
|
+
const originalRetry = msg.retry?.bind(msg);
|
|
314
|
+
msg.retry = () => {
|
|
315
|
+
retriedCount++;
|
|
316
|
+
return originalRetry({
|
|
317
|
+
delaySeconds: calculateExponentialBackoff(
|
|
318
|
+
msg.attempts,
|
|
319
|
+
options.baseDelay
|
|
320
|
+
)
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
const result = await originalMethod.apply(this, args);
|
|
325
|
+
if (typeof this.logQueueRetries === "function") {
|
|
326
|
+
this.logQueueRetries({
|
|
327
|
+
message: `Retried ${retriedCount} out of ${batch.messages.length} messages`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function develitWorker(Worker) {
|
|
336
|
+
class DevelitWorker extends Worker {
|
|
337
|
+
name = "not-set";
|
|
338
|
+
action = "not-set";
|
|
339
|
+
// public db!: DatabaseTransaction<string>
|
|
340
|
+
async fetch() {
|
|
341
|
+
return new Response("Service is up and running!");
|
|
342
|
+
}
|
|
343
|
+
// initializeDB<TAuditAction = string>(
|
|
344
|
+
// database: D1Database,
|
|
345
|
+
// schema: Record<string, unknown>,
|
|
346
|
+
// auditLogWriter?: AuditLogWriter<TAuditAction>,
|
|
347
|
+
// ): void {
|
|
348
|
+
// const drizzleInstance = drizzle(database, {
|
|
349
|
+
// schema,
|
|
350
|
+
// }) as DrizzleD1Database<Record<string, unknown>>
|
|
351
|
+
// this.db = new DatabaseTransaction<TAuditAction>(
|
|
352
|
+
// drizzleInstance,
|
|
353
|
+
// this.name,
|
|
354
|
+
// auditLogWriter,
|
|
355
|
+
// ) as unknown as DatabaseTransaction<string>
|
|
356
|
+
// }
|
|
357
|
+
handleInput({
|
|
358
|
+
input,
|
|
359
|
+
schema
|
|
360
|
+
}) {
|
|
361
|
+
this.logInput({ input });
|
|
362
|
+
const parseResult = z.safeParse(schema, input);
|
|
363
|
+
if (!parseResult.success) {
|
|
364
|
+
const parseError = {
|
|
365
|
+
status: 400,
|
|
366
|
+
code: "INVALID_ACTION_INPUT",
|
|
367
|
+
message: z.prettifyError(parseResult.error)
|
|
368
|
+
};
|
|
369
|
+
this.logError(parseError);
|
|
370
|
+
throw RPCResponse.validationError(parseError);
|
|
371
|
+
}
|
|
372
|
+
if (!parseResult.data) {
|
|
373
|
+
throw createInternalError({
|
|
374
|
+
message: "The provided input could not be processed."
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return parseResult.data;
|
|
378
|
+
}
|
|
379
|
+
log(data, identifier) {
|
|
380
|
+
const name = identifier ?? `${this.name}:${this.action}:log`;
|
|
381
|
+
console.log(name, {
|
|
382
|
+
entrypoint: this.name,
|
|
383
|
+
action: this.action,
|
|
384
|
+
identifier: name,
|
|
385
|
+
data: superjson.stringify(data)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
logQueuePush(data) {
|
|
389
|
+
this.log(data, `${this.name}:${this.action}:queue-push`);
|
|
390
|
+
}
|
|
391
|
+
logQueuePull(data) {
|
|
392
|
+
this.log(data, `${this.name}:${this.action}:queue-pull`);
|
|
393
|
+
}
|
|
394
|
+
logQueueRetries(data) {
|
|
395
|
+
this.log(data, `${this.name}:${this.action}:queue-retries`);
|
|
396
|
+
}
|
|
397
|
+
logInput(data) {
|
|
398
|
+
this.log(data, `${this.name}:${this.action}:input`);
|
|
399
|
+
}
|
|
400
|
+
logOutput(data) {
|
|
401
|
+
this.log(data, `${this.name}:${this.action}:output`);
|
|
402
|
+
}
|
|
403
|
+
logError(error) {
|
|
404
|
+
this.log(error, `${this.name}:${this.action}:error`);
|
|
405
|
+
}
|
|
406
|
+
pushToQueue(queue, message) {
|
|
407
|
+
if (!Array.isArray(message))
|
|
408
|
+
return queue.send(message, { contentType: "v8" });
|
|
409
|
+
return queue.sendBatch(
|
|
410
|
+
message.map((m) => ({
|
|
411
|
+
body: m,
|
|
412
|
+
contentType: "v8"
|
|
413
|
+
}))
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
async handleAction(input, options = {}, actionExecution) {
|
|
417
|
+
return handleAction(this, input, options, actionExecution);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return DevelitWorker;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export { DatabaseTransaction, RPCResponse, action, base, basePostgres, calculateExponentialBackoff, cloudflareQueue, createAuditLogWriter, createInternalError, defineCommand, develitWorker, drizzleConfig, first, firstOrError, handleAction, handleActionResponse, ibanZodSchema, isInternalError, service, swiftZodSchema, useResult, useResultSync, uuidv4 };
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@develit-io/backend-sdk",
|
|
3
|
+
"version": "5.4.5",
|
|
4
|
+
"description": "Develit Backend SDK",
|
|
5
|
+
"author": "Develit.io",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"prepack": "unbuild",
|
|
10
|
+
"typecheck": "tsc",
|
|
11
|
+
"lint": "biome check",
|
|
12
|
+
"lint:fix": "biome check --fix",
|
|
13
|
+
"build": "unbuild",
|
|
14
|
+
"changelogen": "bunx changelogen@latest --bump",
|
|
15
|
+
"release": "bun run build && bunx changelogen@latest --release --push && npm publish --access public"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./dist/index.mjs",
|
|
20
|
+
"require": "./dist/index.cjs"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.cjs",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": ["dist"],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@cloudflare/workers-types": "^4.20250407.0",
|
|
29
|
+
"consola": "^3.4.2",
|
|
30
|
+
"drizzle-kit": "^0.30.6",
|
|
31
|
+
"drizzle-orm": "^0.44.3",
|
|
32
|
+
"h3": "^1.15.3",
|
|
33
|
+
"http-status-codes": "2.3.0",
|
|
34
|
+
"superjson": "^2.2.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"zod": "^4.0.5"
|
|
38
|
+
}
|
|
39
|
+
}
|