@primstack/cli 0.0.1

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.
Files changed (210) hide show
  1. package/dist/generators/crud/templates/drizzle-table.ts.template +12 -0
  2. package/dist/generators/crud/templates/handlers.ts.template +136 -0
  3. package/dist/generators/crud/templates/routes.ts.template +21 -0
  4. package/dist/generators/crud/templates/schema.ts.template +20 -0
  5. package/dist/index.js +9618 -0
  6. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/index.ts.template +79 -0
  7. package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/types.ts.template +12 -0
  8. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/index.ts.template +62 -0
  9. package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/types.ts.template +12 -0
  10. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/index.ts.template +67 -0
  11. package/dist/integrations/analytics/providers/posthog/templates/src/analytics/types.ts.template +12 -0
  12. package/dist/integrations/auth/providers/authjoy/templates/api/src/middleware/auth.ts.template +89 -0
  13. package/dist/integrations/auth/providers/authjoy/templates/api/src/routes/auth.ts.template +27 -0
  14. package/dist/integrations/auth/providers/authjoy/templates/web/src/components/AuthProvider.tsx.template +40 -0
  15. package/dist/integrations/auth/providers/authjoy/templates/web/src/hooks/use-auth.ts.template +71 -0
  16. package/dist/integrations/auth/providers/authjoy/templates/web/src/lib/auth.ts.template +59 -0
  17. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/account.tsx.template +84 -0
  18. package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/login.tsx.template +73 -0
  19. package/dist/integrations/cache/providers/memory/templates/src/cache/index.ts.template +43 -0
  20. package/dist/integrations/cache/providers/memory/templates/src/cache/types.ts.template +3 -0
  21. package/dist/integrations/cache/providers/redis/templates/src/cache/index.ts.template +37 -0
  22. package/dist/integrations/cache/providers/redis/templates/src/cache/types.ts.template +3 -0
  23. package/dist/integrations/cache/providers/valkey/templates/src/cache/index.ts.template +38 -0
  24. package/dist/integrations/cache/providers/valkey/templates/src/cache/types.ts.template +3 -0
  25. package/dist/integrations/db/providers/postgres/templates/drizzle.config.ts.template +10 -0
  26. package/dist/integrations/db/providers/postgres/templates/src/db/index.ts.template +13 -0
  27. package/dist/integrations/db/providers/postgres/templates/src/db/migrate.ts.template +19 -0
  28. package/dist/integrations/db/providers/postgres/templates/src/db/schema/index.ts.template +1 -0
  29. package/dist/integrations/db/providers/postgres/templates/src/db/schema/users.ts.template +12 -0
  30. package/dist/integrations/db/providers/postgres/templates/src/db/seed.ts.template +28 -0
  31. package/dist/integrations/db/providers/sqlite/templates/drizzle.config.ts.template +10 -0
  32. package/dist/integrations/db/providers/sqlite/templates/src/db/index.ts.template +10 -0
  33. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/index.ts.template +1 -0
  34. package/dist/integrations/db/providers/sqlite/templates/src/db/schema/users.ts.template +12 -0
  35. package/dist/integrations/db/providers/sqlite/templates/src/db/seed.ts.template +28 -0
  36. package/dist/integrations/db/providers/supabase/templates/drizzle.config.ts.template +10 -0
  37. package/dist/integrations/db/providers/supabase/templates/src/db/index.ts.template +13 -0
  38. package/dist/integrations/db/providers/supabase/templates/src/db/migrate.ts.template +19 -0
  39. package/dist/integrations/db/providers/supabase/templates/src/db/schema/index.ts.template +1 -0
  40. package/dist/integrations/db/providers/supabase/templates/src/db/schema/users.ts.template +12 -0
  41. package/dist/integrations/db/providers/supabase/templates/src/db/seed.ts.template +28 -0
  42. package/dist/integrations/db/providers/turso/templates/drizzle.config.ts.template +11 -0
  43. package/dist/integrations/db/providers/turso/templates/src/db/index.ts.template +14 -0
  44. package/dist/integrations/db/providers/turso/templates/src/db/schema/index.ts.template +1 -0
  45. package/dist/integrations/db/providers/turso/templates/src/db/schema/users.ts.template +12 -0
  46. package/dist/integrations/db/providers/turso/templates/src/db/seed.ts.template +28 -0
  47. package/dist/integrations/email/providers/nodemailer/templates/src/email/index.ts.template +24 -0
  48. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/index.ts.template +1 -0
  49. package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/welcome.ts.template +7 -0
  50. package/dist/integrations/email/providers/nodemailer/templates/src/email/types.ts.template +7 -0
  51. package/dist/integrations/email/providers/resend/templates/src/email/index.ts.template +18 -0
  52. package/dist/integrations/email/providers/resend/templates/src/email/templates/index.ts.template +1 -0
  53. package/dist/integrations/email/providers/resend/templates/src/email/templates/welcome.ts.template +7 -0
  54. package/dist/integrations/email/providers/resend/templates/src/email/types.ts.template +7 -0
  55. package/dist/integrations/email/providers/sendgrid/templates/src/email/index.ts.template +16 -0
  56. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/index.ts.template +1 -0
  57. package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/welcome.ts.template +7 -0
  58. package/dist/integrations/email/providers/sendgrid/templates/src/email/types.ts.template +7 -0
  59. package/dist/integrations/flags/providers/local/templates/api/src/lib/flags.ts.template +97 -0
  60. package/dist/integrations/flags/providers/local/templates/api/src/routes/flags.ts.template +36 -0
  61. package/dist/integrations/flags/providers/local/templates/flags.json.template +8 -0
  62. package/dist/integrations/flags/providers/local/templates/web/src/hooks/use-flag.ts.template +60 -0
  63. package/dist/integrations/logging/providers/axiom/templates/src/logging/index.ts.template +56 -0
  64. package/dist/integrations/logging/providers/axiom/templates/src/logging/types.ts.template +5 -0
  65. package/dist/integrations/logging/providers/pino/templates/src/logging/index.ts.template +21 -0
  66. package/dist/integrations/logging/providers/pino/templates/src/logging/types.ts.template +5 -0
  67. package/dist/integrations/logging/providers/winston/templates/src/logging/index.ts.template +30 -0
  68. package/dist/integrations/logging/providers/winston/templates/src/logging/types.ts.template +5 -0
  69. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/index.ts.template +78 -0
  70. package/dist/integrations/monitor/providers/datadog/templates/src/monitor/types.ts.template +12 -0
  71. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/index.ts.template +60 -0
  72. package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/types.ts.template +12 -0
  73. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/index.ts.template +70 -0
  74. package/dist/integrations/monitor/providers/sentry/templates/src/monitor/types.ts.template +12 -0
  75. package/dist/integrations/queue/providers/bullmq/templates/src/queue/index.ts.template +56 -0
  76. package/dist/integrations/queue/providers/bullmq/templates/src/queue/types.ts.template +10 -0
  77. package/dist/integrations/queue/providers/memory/templates/src/queue/index.ts.template +73 -0
  78. package/dist/integrations/queue/providers/memory/templates/src/queue/types.ts.template +10 -0
  79. package/dist/integrations/queue/providers/pgboss/templates/src/queue/index.ts.template +34 -0
  80. package/dist/integrations/queue/providers/pgboss/templates/src/queue/types.ts.template +10 -0
  81. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/index.ts.template +95 -0
  82. package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/types.ts.template +12 -0
  83. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/index.ts.template +80 -0
  84. package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/types.ts.template +12 -0
  85. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/index.ts.template +67 -0
  86. package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/types.ts.template +12 -0
  87. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/index.ts.template +81 -0
  88. package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/types.ts.template +10 -0
  89. package/dist/integrations/schedule/providers/croner/templates/src/schedule/index.ts.template +47 -0
  90. package/dist/integrations/schedule/providers/croner/templates/src/schedule/types.ts.template +10 -0
  91. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/index.ts.template +45 -0
  92. package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/types.ts.template +10 -0
  93. package/dist/integrations/search/providers/algolia/templates/src/search/index.ts.template +52 -0
  94. package/dist/integrations/search/providers/algolia/templates/src/search/types.ts.template +18 -0
  95. package/dist/integrations/search/providers/meilisearch/templates/src/search/index.ts.template +49 -0
  96. package/dist/integrations/search/providers/meilisearch/templates/src/search/types.ts.template +18 -0
  97. package/dist/integrations/search/providers/typesense/templates/src/search/index.ts.template +71 -0
  98. package/dist/integrations/search/providers/typesense/templates/src/search/types.ts.template +35 -0
  99. package/dist/integrations/storage/providers/local/templates/src/storage/index.ts.template +69 -0
  100. package/dist/integrations/storage/providers/local/templates/src/storage/types.ts.template +12 -0
  101. package/dist/integrations/storage/providers/r2/templates/src/storage/index.ts.template +80 -0
  102. package/dist/integrations/storage/providers/r2/templates/src/storage/types.ts.template +12 -0
  103. package/dist/integrations/storage/providers/s3/templates/src/storage/index.ts.template +78 -0
  104. package/dist/integrations/storage/providers/s3/templates/src/storage/types.ts.template +12 -0
  105. package/dist/integrations/stripe/templates/api/src/lib/stripe.ts.template +259 -0
  106. package/dist/integrations/stripe/templates/api/src/routes/stripe-webhooks.ts.template +284 -0
  107. package/dist/integrations/stripe/templates/api/stripe.config.ts.template +178 -0
  108. package/dist/integrations/stripe/templates/shared/src/pricing.ts.template +117 -0
  109. package/dist/integrations/stripe/templates/shared/src/stripe-types.ts.template +133 -0
  110. package/dist/integrations/stripe/templates/web/src/components/billing-settings.tsx.template +123 -0
  111. package/dist/integrations/stripe/templates/web/src/components/pricing-cards.tsx.template +115 -0
  112. package/dist/integrations/stripe/templates/web/src/pages/pricing.tsx.template +95 -0
  113. package/dist/templates/api/fastify/.env.example.template +7 -0
  114. package/dist/templates/api/fastify/.gitignore.template +24 -0
  115. package/dist/templates/api/fastify/package.json.template +23 -0
  116. package/dist/templates/api/fastify/src/index.ts.template +52 -0
  117. package/dist/templates/api/fastify/src/lib/env.ts.template +20 -0
  118. package/dist/templates/api/fastify/src/routes/health.ts.template +12 -0
  119. package/dist/templates/api/fastify/tsconfig.json.template +18 -0
  120. package/dist/templates/api/fastify-postgres/.env.example.template +10 -0
  121. package/dist/templates/api/fastify-postgres/drizzle.config.ts.template +10 -0
  122. package/dist/templates/api/fastify-postgres/package.json.template +16 -0
  123. package/dist/templates/api/fastify-postgres/src/db/index.ts.template +9 -0
  124. package/dist/templates/api/fastify-postgres/src/db/schema/users.ts.template +12 -0
  125. package/dist/templates/api/fastify-sqlite/.env.example.template +10 -0
  126. package/dist/templates/api/fastify-sqlite/drizzle.config.ts.template +10 -0
  127. package/dist/templates/api/fastify-sqlite/package.json.template +16 -0
  128. package/dist/templates/api/fastify-sqlite/src/db/index.ts.template +6 -0
  129. package/dist/templates/api/fastify-sqlite/src/db/schema/users.ts.template +12 -0
  130. package/dist/templates/api/fastify-supabase/.env.example.template +10 -0
  131. package/dist/templates/api/fastify-supabase/drizzle.config.ts.template +10 -0
  132. package/dist/templates/api/fastify-supabase/package.json.template +15 -0
  133. package/dist/templates/api/fastify-supabase/src/db/index.ts.template +9 -0
  134. package/dist/templates/api/fastify-supabase/src/db/schema/users.ts.template +12 -0
  135. package/dist/templates/api/fastify-turso/.env.example.template +11 -0
  136. package/dist/templates/api/fastify-turso/drizzle.config.ts.template +11 -0
  137. package/dist/templates/api/fastify-turso/package.json.template +15 -0
  138. package/dist/templates/api/fastify-turso/src/db/index.ts.template +10 -0
  139. package/dist/templates/api/fastify-turso/src/db/schema/users.ts.template +12 -0
  140. package/dist/templates/fullstack/api/.env.example.template +10 -0
  141. package/dist/templates/fullstack/api/.gitignore.template +4 -0
  142. package/dist/templates/fullstack/api/drizzle.config.ts.template +14 -0
  143. package/dist/templates/fullstack/api/package.json.template +33 -0
  144. package/dist/templates/fullstack/api/src/db/index.ts.template +13 -0
  145. package/dist/templates/fullstack/api/src/db/schema/api-keys.ts.template +19 -0
  146. package/dist/templates/fullstack/api/src/db/schema/audit-logs.ts.template +23 -0
  147. package/dist/templates/fullstack/api/src/db/schema/index.ts.template +8 -0
  148. package/dist/templates/fullstack/api/src/db/schema/invites.ts.template +19 -0
  149. package/dist/templates/fullstack/api/src/db/schema/memberships.ts.template +16 -0
  150. package/dist/templates/fullstack/api/src/db/schema/organizations.ts.template +13 -0
  151. package/dist/templates/fullstack/api/src/db/schema/plans.ts.template +29 -0
  152. package/dist/templates/fullstack/api/src/db/schema/subscriptions.ts.template +38 -0
  153. package/dist/templates/fullstack/api/src/db/schema/users.ts.template +14 -0
  154. package/dist/templates/fullstack/api/src/index.ts.template +54 -0
  155. package/dist/templates/fullstack/api/src/lib/env.ts.template +22 -0
  156. package/dist/templates/fullstack/api/src/routes/health.ts.template +14 -0
  157. package/dist/templates/fullstack/api/tsconfig.json.template +15 -0
  158. package/dist/templates/fullstack/root/.gitignore.template +26 -0
  159. package/dist/templates/fullstack/root/package.json.template +15 -0
  160. package/dist/templates/fullstack/root/pnpm-workspace.yaml.template +3 -0
  161. package/dist/templates/fullstack/root/turbo.json.template +17 -0
  162. package/dist/templates/fullstack/shared/package.json.template +36 -0
  163. package/dist/templates/fullstack/shared/src/index.ts.template +8 -0
  164. package/dist/templates/fullstack/shared/src/schemas/api-key.ts.template +28 -0
  165. package/dist/templates/fullstack/shared/src/schemas/audit-log.ts.template +41 -0
  166. package/dist/templates/fullstack/shared/src/schemas/index.ts.template +8 -0
  167. package/dist/templates/fullstack/shared/src/schemas/invite.ts.template +25 -0
  168. package/dist/templates/fullstack/shared/src/schemas/membership.ts.template +20 -0
  169. package/dist/templates/fullstack/shared/src/schemas/organization.ts.template +18 -0
  170. package/dist/templates/fullstack/shared/src/schemas/plan.ts.template +38 -0
  171. package/dist/templates/fullstack/shared/src/schemas/subscription.ts.template +56 -0
  172. package/dist/templates/fullstack/shared/src/schemas/user.ts.template +21 -0
  173. package/dist/templates/fullstack/shared/src/types/index.ts.template +75 -0
  174. package/dist/templates/fullstack/shared/src/validators/index.ts.template +53 -0
  175. package/dist/templates/fullstack/shared/tsconfig.json.template +17 -0
  176. package/dist/templates/fullstack/web/.gitignore.template +3 -0
  177. package/dist/templates/fullstack/web/index.html.template +13 -0
  178. package/dist/templates/fullstack/web/package.json.template +23 -0
  179. package/dist/templates/fullstack/web/src/App.tsx.template +47 -0
  180. package/dist/templates/fullstack/web/src/index.css.template +54 -0
  181. package/dist/templates/fullstack/web/src/main.tsx.template +10 -0
  182. package/dist/templates/fullstack/web/src/vite-env.d.ts.template +1 -0
  183. package/dist/templates/fullstack/web/tsconfig.json.template +21 -0
  184. package/dist/templates/fullstack/web/tsconfig.node.json.template +11 -0
  185. package/dist/templates/fullstack/web/vite.config.ts.template +15 -0
  186. package/dist/templates/hosted/root/.env.local.template +13 -0
  187. package/dist/templates/hosted/root/.gitignore.template +32 -0
  188. package/dist/templates/hosted/root/CLAUDE.md.template +139 -0
  189. package/dist/templates/hosted/root/drizzle.config.ts.template +10 -0
  190. package/dist/templates/hosted/root/next.config.ts.template +15 -0
  191. package/dist/templates/hosted/root/package.json.template +40 -0
  192. package/dist/templates/hosted/root/postcss.config.mjs.template +9 -0
  193. package/dist/templates/hosted/root/primstack.config.json.template +5 -0
  194. package/dist/templates/hosted/root/tailwind.config.ts.template +14 -0
  195. package/dist/templates/hosted/root/tsconfig.json.template +25 -0
  196. package/dist/templates/hosted/root/wrangler.toml.template +9 -0
  197. package/dist/templates/hosted/src/app/actions/example.ts.template +50 -0
  198. package/dist/templates/hosted/src/app/api/health/route.ts.template +5 -0
  199. package/dist/templates/hosted/src/app/auth/login/page.tsx.template +32 -0
  200. package/dist/templates/hosted/src/app/globals.css.template +59 -0
  201. package/dist/templates/hosted/src/app/layout.tsx.template +24 -0
  202. package/dist/templates/hosted/src/app/page.tsx.template +34 -0
  203. package/dist/templates/hosted/src/db/migrations/0000_initial.sql.template +43 -0
  204. package/dist/templates/hosted/src/db/schema.ts.template +52 -0
  205. package/dist/templates/hosted/src/env.d.ts.template +10 -0
  206. package/dist/templates/hosted/src/instrumentation.ts.template +6 -0
  207. package/dist/templates/hosted/src/lib/auth.ts.template +35 -0
  208. package/dist/templates/hosted/src/lib/db.ts.template +17 -0
  209. package/dist/templates/hosted/src/middleware.ts.template +6 -0
  210. package/package.json +46 -0
@@ -0,0 +1,73 @@
1
+ import { EventEmitter } from 'events';
2
+ import type { JobOptions, JobHandler } from './types.js';
3
+
4
+ interface QueuedJob<T = unknown> {
5
+ id: string;
6
+ name: string;
7
+ data: T;
8
+ options: JobOptions;
9
+ attempts: number;
10
+ }
11
+
12
+ const emitter = new EventEmitter();
13
+ const handlers = new Map<string, JobHandler<unknown>>();
14
+ const timers = new Set<ReturnType<typeof setTimeout>>();
15
+ let jobCounter = 0;
16
+
17
+ export async function addJob<T = unknown>(
18
+ name: string,
19
+ data: T,
20
+ options?: JobOptions,
21
+ ): Promise<string> {
22
+ const id = `job_${++jobCounter}`;
23
+ const job: QueuedJob<T> = {
24
+ id,
25
+ name,
26
+ data,
27
+ options: options ?? {},
28
+ attempts: 0,
29
+ };
30
+
31
+ const delay = options?.delay ?? 0;
32
+ const timer = setTimeout(() => {
33
+ timers.delete(timer);
34
+ emitter.emit(name, job);
35
+ }, delay);
36
+ timers.add(timer);
37
+
38
+ return id;
39
+ }
40
+
41
+ export async function registerWorker<T = unknown>(
42
+ name: string,
43
+ handler: JobHandler<T>,
44
+ ): Promise<void> {
45
+ handlers.set(name, handler as JobHandler<unknown>);
46
+ emitter.on(name, async (job: QueuedJob<T>) => {
47
+ const maxAttempts = job.options.attempts ?? 3;
48
+ const backoff = job.options.backoff ?? 1000;
49
+
50
+ while (job.attempts < maxAttempts) {
51
+ try {
52
+ job.attempts++;
53
+ await handler(job.data);
54
+ return;
55
+ } catch (err) {
56
+ if (job.attempts >= maxAttempts) {
57
+ console.error(`[queue] Job ${job.id} (${name}) failed after ${maxAttempts} attempts:`, err);
58
+ return;
59
+ }
60
+ await new Promise((r) => setTimeout(r, backoff));
61
+ }
62
+ }
63
+ });
64
+ }
65
+
66
+ export async function stopAll(): Promise<void> {
67
+ for (const timer of timers) clearTimeout(timer);
68
+ timers.clear();
69
+ emitter.removeAllListeners();
70
+ handlers.clear();
71
+ }
72
+
73
+ export type { JobOptions, JobHandler } from './types.js';
@@ -0,0 +1,10 @@
1
+ export interface JobOptions {
2
+ delay?: number; // ms before job becomes processable
3
+ attempts?: number; // max retry attempts (default: 3)
4
+ backoff?: number; // ms between retries
5
+ priority?: number; // lower = higher priority
6
+ }
7
+
8
+ export interface JobHandler<T = unknown> {
9
+ (data: T): Promise<void>;
10
+ }
@@ -0,0 +1,34 @@
1
+ import PgBoss from 'pg-boss';
2
+ import type { JobOptions, JobHandler } from './types.js';
3
+
4
+ export const boss = new PgBoss(process.env.DATABASE_URL || 'postgresql://localhost:5432/mydb');
5
+
6
+ export async function startBoss(): Promise<void> {
7
+ await boss.start();
8
+ }
9
+
10
+ export async function addJob<T extends object = object>(
11
+ name: string,
12
+ data: T,
13
+ options?: JobOptions,
14
+ ): Promise<string | null> {
15
+ return boss.send(name, data, {
16
+ startAfter: options?.delay ? new Date(Date.now() + options.delay) : undefined,
17
+ retryLimit: options?.attempts ?? 3,
18
+ retryDelay: options?.backoff ? options.backoff / 1000 : undefined,
19
+ priority: options?.priority,
20
+ });
21
+ }
22
+
23
+ export async function registerWorker<T extends object = object>(
24
+ name: string,
25
+ handler: JobHandler<T>,
26
+ ): Promise<void> {
27
+ await boss.work(name, async (job) => handler(job.data as T));
28
+ }
29
+
30
+ export async function stopBoss(): Promise<void> {
31
+ await boss.stop();
32
+ }
33
+
34
+ export type { JobOptions, JobHandler } from './types.js';
@@ -0,0 +1,10 @@
1
+ export interface JobOptions {
2
+ delay?: number; // ms before job becomes processable
3
+ attempts?: number; // max retry attempts (default: 3)
4
+ backoff?: number; // ms between retries
5
+ priority?: number; // lower = higher priority
6
+ }
7
+
8
+ export interface JobHandler<T = unknown> {
9
+ (data: T): Promise<void>;
10
+ }
@@ -0,0 +1,95 @@
1
+ import type { RateLimiterOptions, RateLimitResult } from './types.js';
2
+
3
+ export type { RateLimiterOptions, RateLimitResult } from './types.js';
4
+
5
+ interface WindowEntry {
6
+ count: number;
7
+ resetAt: number;
8
+ }
9
+
10
+ export interface RateLimiter {
11
+ points: number;
12
+ duration: number;
13
+ store: Map<string, WindowEntry>;
14
+ }
15
+
16
+ /**
17
+ * Create a rate limiter instance.
18
+ * Defaults: 10 requests per 1 second window.
19
+ */
20
+ export function createRateLimiter(options?: Partial<RateLimiterOptions>): RateLimiter {
21
+ return {
22
+ points: options?.points ?? 10,
23
+ duration: options?.duration ?? 1,
24
+ store: new Map(),
25
+ };
26
+ }
27
+
28
+ function getEntry(limiter: RateLimiter, key: string): WindowEntry {
29
+ const now = Date.now();
30
+ const entry = limiter.store.get(key);
31
+
32
+ if (!entry || now >= entry.resetAt) {
33
+ const newEntry: WindowEntry = {
34
+ count: 0,
35
+ resetAt: now + limiter.duration * 1000,
36
+ };
37
+ limiter.store.set(key, newEntry);
38
+ return newEntry;
39
+ }
40
+
41
+ return entry;
42
+ }
43
+
44
+ /**
45
+ * Consume points from the rate limiter for the given key.
46
+ */
47
+ export async function limit(
48
+ limiter: RateLimiter,
49
+ key: string,
50
+ points?: number,
51
+ ): Promise<RateLimitResult> {
52
+ const entry = getEntry(limiter, key);
53
+ const cost = points ?? 1;
54
+
55
+ if (entry.count + cost > limiter.points) {
56
+ return {
57
+ allowed: false,
58
+ remaining: Math.max(0, limiter.points - entry.count),
59
+ limit: limiter.points,
60
+ resetMs: entry.resetAt - Date.now(),
61
+ };
62
+ }
63
+
64
+ entry.count += cost;
65
+
66
+ return {
67
+ allowed: true,
68
+ remaining: limiter.points - entry.count,
69
+ limit: limiter.points,
70
+ resetMs: entry.resetAt - Date.now(),
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Check rate limit status without consuming a point.
76
+ */
77
+ export async function check(
78
+ limiter: RateLimiter,
79
+ key: string,
80
+ ): Promise<RateLimitResult> {
81
+ const entry = getEntry(limiter, key);
82
+ return {
83
+ allowed: entry.count < limiter.points,
84
+ remaining: Math.max(0, limiter.points - entry.count),
85
+ limit: limiter.points,
86
+ resetMs: entry.resetAt - Date.now(),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Reset the rate limit for a given key.
92
+ */
93
+ export async function reset(limiter: RateLimiter, key: string): Promise<void> {
94
+ limiter.store.delete(key);
95
+ }
@@ -0,0 +1,12 @@
1
+ export interface RateLimiterOptions {
2
+ points: number;
3
+ duration: number;
4
+ blockDuration?: number;
5
+ }
6
+
7
+ export interface RateLimitResult {
8
+ allowed: boolean;
9
+ remaining: number;
10
+ limit: number;
11
+ resetMs: number;
12
+ }
@@ -0,0 +1,80 @@
1
+ import { RateLimiterMemory } from 'rate-limiter-flexible';
2
+ import type { RateLimiterOptions, RateLimitResult } from './types.js';
3
+
4
+ export type { RateLimiterOptions, RateLimitResult } from './types.js';
5
+
6
+ export type RateLimiter = RateLimiterMemory;
7
+
8
+ /**
9
+ * Create a rate limiter instance.
10
+ * Defaults: 10 requests per 1 second window.
11
+ */
12
+ export function createRateLimiter(options?: Partial<RateLimiterOptions>): RateLimiter {
13
+ const points = options?.points ?? 10;
14
+ const duration = options?.duration ?? 1;
15
+ const blockDuration = options?.blockDuration ?? 0;
16
+
17
+ return new RateLimiterMemory({
18
+ points,
19
+ duration,
20
+ blockDuration,
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Consume points from the rate limiter for the given key.
26
+ */
27
+ export async function limit(
28
+ limiter: RateLimiter,
29
+ key: string,
30
+ points?: number,
31
+ ): Promise<RateLimitResult> {
32
+ try {
33
+ const res = await limiter.consume(key, points ?? 1);
34
+ return {
35
+ allowed: true,
36
+ remaining: res.remainingPoints,
37
+ limit: limiter.points,
38
+ resetMs: res.msBeforeNext,
39
+ };
40
+ } catch (rej: unknown) {
41
+ const res = rej as { remainingPoints: number; msBeforeNext: number };
42
+ return {
43
+ allowed: false,
44
+ remaining: res.remainingPoints,
45
+ limit: limiter.points,
46
+ resetMs: res.msBeforeNext,
47
+ };
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check rate limit status without consuming a point.
53
+ */
54
+ export async function check(
55
+ limiter: RateLimiter,
56
+ key: string,
57
+ ): Promise<RateLimitResult> {
58
+ const res = await limiter.get(key);
59
+ if (!res) {
60
+ return {
61
+ allowed: true,
62
+ remaining: limiter.points,
63
+ limit: limiter.points,
64
+ resetMs: 0,
65
+ };
66
+ }
67
+ return {
68
+ allowed: res.remainingPoints >= 0,
69
+ remaining: res.remainingPoints,
70
+ limit: limiter.points,
71
+ resetMs: res.msBeforeNext,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Reset the rate limit for a given key.
77
+ */
78
+ export async function reset(limiter: RateLimiter, key: string): Promise<void> {
79
+ await limiter.delete(key);
80
+ }
@@ -0,0 +1,12 @@
1
+ export interface RateLimiterOptions {
2
+ points: number;
3
+ duration: number;
4
+ blockDuration?: number;
5
+ }
6
+
7
+ export interface RateLimitResult {
8
+ allowed: boolean;
9
+ remaining: number;
10
+ limit: number;
11
+ resetMs: number;
12
+ }
@@ -0,0 +1,67 @@
1
+ import { Ratelimit } from '@upstash/ratelimit';
2
+ import { Redis } from '@upstash/redis';
3
+ import type { RateLimiterOptions, RateLimitResult } from './types.js';
4
+
5
+ export type { RateLimiterOptions, RateLimitResult } from './types.js';
6
+
7
+ export type RateLimiter = Ratelimit;
8
+
9
+ const redis = new Redis({
10
+ url: process.env.UPSTASH_REDIS_REST_URL!,
11
+ token: process.env.UPSTASH_REDIS_REST_TOKEN!,
12
+ });
13
+
14
+ /**
15
+ * Create a rate limiter instance.
16
+ * Defaults: 10 requests per 1 second sliding window.
17
+ */
18
+ export function createRateLimiter(options?: Partial<RateLimiterOptions>): RateLimiter {
19
+ const points = options?.points ?? 10;
20
+ const duration = options?.duration ?? 1;
21
+
22
+ return new Ratelimit({
23
+ redis,
24
+ limiter: Ratelimit.slidingWindow(points, `${duration} s`),
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Consume points from the rate limiter for the given key.
30
+ * Note: Upstash ratelimit consumes 1 token per call; the points parameter is ignored.
31
+ */
32
+ export async function limit(
33
+ limiter: RateLimiter,
34
+ key: string,
35
+ _points?: number,
36
+ ): Promise<RateLimitResult> {
37
+ const res = await limiter.limit(key);
38
+ return {
39
+ allowed: res.success,
40
+ remaining: res.remaining,
41
+ limit: res.limit,
42
+ resetMs: Math.max(0, res.reset - Date.now()),
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Check rate limit status without consuming a point.
48
+ */
49
+ export async function check(
50
+ limiter: RateLimiter,
51
+ key: string,
52
+ ): Promise<RateLimitResult> {
53
+ const res = await limiter.getRemaining(key);
54
+ return {
55
+ allowed: res.remaining > 0,
56
+ remaining: res.remaining,
57
+ limit: 0,
58
+ resetMs: Math.max(0, res.reset - Date.now()),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Reset the rate limit for a given key.
64
+ */
65
+ export async function reset(limiter: RateLimiter, key: string): Promise<void> {
66
+ await limiter.resetUsedTokens(key);
67
+ }
@@ -0,0 +1,12 @@
1
+ export interface RateLimiterOptions {
2
+ points: number;
3
+ duration: number;
4
+ blockDuration?: number;
5
+ }
6
+
7
+ export interface RateLimitResult {
8
+ allowed: boolean;
9
+ remaining: number;
10
+ limit: number;
11
+ resetMs: number;
12
+ }
@@ -0,0 +1,81 @@
1
+ import { Queue, Worker, type Job } from 'bullmq';
2
+ import IORedis from 'ioredis';
3
+ import type { ScheduledJob, SchedulerOptions } from './types.js';
4
+
5
+ const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
6
+ const connection = new IORedis(redisUrl, { maxRetriesPerRequest: null });
7
+
8
+ const QUEUE_NAME = 'scheduled-jobs';
9
+ const queue = new Queue(QUEUE_NAME, { connection });
10
+
11
+ const handlers = new Map<string, () => void | Promise<void>>();
12
+ let worker: Worker | null = null;
13
+
14
+ function ensureWorker(): void {
15
+ if (worker) return;
16
+
17
+ worker = new Worker(
18
+ QUEUE_NAME,
19
+ async (job: Job) => {
20
+ const handler = handlers.get(job.name);
21
+ if (handler) {
22
+ await handler();
23
+ }
24
+ },
25
+ { connection },
26
+ );
27
+ }
28
+
29
+ export async function scheduleJob(
30
+ name: string,
31
+ cronExpr: string,
32
+ handler: () => void | Promise<void>,
33
+ options?: SchedulerOptions,
34
+ ): Promise<void> {
35
+ if (handlers.has(name)) {
36
+ throw new Error(`Job '${name}' is already scheduled`);
37
+ }
38
+
39
+ handlers.set(name, handler);
40
+ ensureWorker();
41
+
42
+ await queue.upsertJobScheduler(
43
+ name,
44
+ { pattern: cronExpr, ...(options?.timezone ? { tz: options.timezone } : {}) },
45
+ { name },
46
+ );
47
+
48
+ if (options?.runOnInit) {
49
+ await queue.add(name, {});
50
+ }
51
+ }
52
+
53
+ export async function stopJob(name: string): Promise<void> {
54
+ if (!handlers.has(name)) {
55
+ throw new Error(`Job '${name}' not found`);
56
+ }
57
+
58
+ await queue.removeJobScheduler(name);
59
+ handlers.delete(name);
60
+ }
61
+
62
+ export async function stopAll(): Promise<void> {
63
+ for (const name of handlers.keys()) {
64
+ await queue.removeJobScheduler(name);
65
+ }
66
+ handlers.clear();
67
+
68
+ if (worker) {
69
+ await worker.close();
70
+ worker = null;
71
+ }
72
+
73
+ await queue.close();
74
+ await connection.quit();
75
+ }
76
+
77
+ export function listJobs(): string[] {
78
+ return Array.from(handlers.keys());
79
+ }
80
+
81
+ export type { ScheduledJob, SchedulerOptions } from './types.js';
@@ -0,0 +1,10 @@
1
+ export interface ScheduledJob {
2
+ name: string;
3
+ schedule: string;
4
+ handler: () => void | Promise<void>;
5
+ }
6
+
7
+ export interface SchedulerOptions {
8
+ timezone?: string;
9
+ runOnInit?: boolean;
10
+ }
@@ -0,0 +1,47 @@
1
+ import { Cron } from 'croner';
2
+ import type { ScheduledJob, SchedulerOptions } from './types.js';
3
+
4
+ const jobs = new Map<string, Cron>();
5
+
6
+ export function scheduleJob(
7
+ name: string,
8
+ cronExpr: string,
9
+ handler: () => void | Promise<void>,
10
+ options?: SchedulerOptions,
11
+ ): void {
12
+ if (jobs.has(name)) {
13
+ throw new Error(`Job '${name}' is already scheduled`);
14
+ }
15
+
16
+ const job = new Cron(cronExpr, {
17
+ timezone: options?.timezone,
18
+ }, handler);
19
+
20
+ if (options?.runOnInit) {
21
+ job.trigger();
22
+ }
23
+
24
+ jobs.set(name, job);
25
+ }
26
+
27
+ export function stopJob(name: string): void {
28
+ const job = jobs.get(name);
29
+ if (!job) {
30
+ throw new Error(`Job '${name}' not found`);
31
+ }
32
+ job.stop();
33
+ jobs.delete(name);
34
+ }
35
+
36
+ export function stopAll(): void {
37
+ for (const [name, job] of jobs) {
38
+ job.stop();
39
+ jobs.delete(name);
40
+ }
41
+ }
42
+
43
+ export function listJobs(): string[] {
44
+ return Array.from(jobs.keys());
45
+ }
46
+
47
+ export type { ScheduledJob, SchedulerOptions } from './types.js';
@@ -0,0 +1,10 @@
1
+ export interface ScheduledJob {
2
+ name: string;
3
+ schedule: string;
4
+ handler: () => void | Promise<void>;
5
+ }
6
+
7
+ export interface SchedulerOptions {
8
+ timezone?: string;
9
+ runOnInit?: boolean;
10
+ }
@@ -0,0 +1,45 @@
1
+ import cron, { type ScheduledTask } from 'node-cron';
2
+ import type { ScheduledJob, SchedulerOptions } from './types.js';
3
+
4
+ const jobs = new Map<string, ScheduledTask>();
5
+
6
+ export function scheduleJob(
7
+ name: string,
8
+ cronExpr: string,
9
+ handler: () => void | Promise<void>,
10
+ options?: SchedulerOptions,
11
+ ): void {
12
+ if (jobs.has(name)) {
13
+ throw new Error(`Job '${name}' is already scheduled`);
14
+ }
15
+
16
+ const task = cron.schedule(cronExpr, handler, {
17
+ timezone: options?.timezone,
18
+ scheduled: true,
19
+ runOnInit: options?.runOnInit ?? false,
20
+ });
21
+
22
+ jobs.set(name, task);
23
+ }
24
+
25
+ export function stopJob(name: string): void {
26
+ const task = jobs.get(name);
27
+ if (!task) {
28
+ throw new Error(`Job '${name}' not found`);
29
+ }
30
+ task.stop();
31
+ jobs.delete(name);
32
+ }
33
+
34
+ export function stopAll(): void {
35
+ for (const [name, task] of jobs) {
36
+ task.stop();
37
+ jobs.delete(name);
38
+ }
39
+ }
40
+
41
+ export function listJobs(): string[] {
42
+ return Array.from(jobs.keys());
43
+ }
44
+
45
+ export type { ScheduledJob, SchedulerOptions } from './types.js';
@@ -0,0 +1,10 @@
1
+ export interface ScheduledJob {
2
+ name: string;
3
+ schedule: string;
4
+ handler: () => void | Promise<void>;
5
+ }
6
+
7
+ export interface SchedulerOptions {
8
+ timezone?: string;
9
+ runOnInit?: boolean;
10
+ }
@@ -0,0 +1,52 @@
1
+ import { algoliasearch } from 'algoliasearch';
2
+ import type { SearchDocument, SearchOptions, SearchResult } from './types.js';
3
+
4
+ export const client = algoliasearch(
5
+ process.env.ALGOLIA_APP_ID || '',
6
+ process.env.ALGOLIA_API_KEY || '',
7
+ );
8
+
9
+ export function getIndex(name: string) {
10
+ return name;
11
+ }
12
+
13
+ export async function addDocuments<T extends SearchDocument>(
14
+ indexName: string,
15
+ documents: T[],
16
+ ): Promise<void> {
17
+ await client.saveObjects({
18
+ indexName,
19
+ objects: documents.map((doc) => ({ objectID: String(doc.id), ...doc })),
20
+ });
21
+ }
22
+
23
+ export async function search<T extends SearchDocument>(
24
+ indexName: string,
25
+ query: string,
26
+ options?: SearchOptions,
27
+ ): Promise<SearchResult<T>> {
28
+ const result = await client.searchSingleIndex({
29
+ indexName,
30
+ searchParams: {
31
+ query,
32
+ hitsPerPage: options?.limit ?? 20,
33
+ page: options?.offset ? Math.floor(options.offset / (options?.limit ?? 20)) : 0,
34
+ filters: options?.filter,
35
+ },
36
+ });
37
+ return {
38
+ hits: result.hits as T[],
39
+ totalHits: result.nbHits ?? result.hits.length,
40
+ query,
41
+ processingTimeMs: result.processingTimeMS ?? 0,
42
+ };
43
+ }
44
+
45
+ export async function deleteDocuments(
46
+ indexName: string,
47
+ ids: (string | number)[],
48
+ ): Promise<void> {
49
+ await client.deleteObjects({ indexName, objectIDs: ids.map(String) });
50
+ }
51
+
52
+ export type { SearchDocument, SearchOptions, SearchResult } from './types.js';
@@ -0,0 +1,18 @@
1
+ export interface SearchDocument {
2
+ id: string | number;
3
+ [key: string]: unknown;
4
+ }
5
+
6
+ export interface SearchOptions {
7
+ limit?: number;
8
+ offset?: number;
9
+ filter?: string;
10
+ sort?: string[];
11
+ }
12
+
13
+ export interface SearchResult<T = SearchDocument> {
14
+ hits: T[];
15
+ totalHits: number;
16
+ query: string;
17
+ processingTimeMs: number;
18
+ }