@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.
- package/dist/generators/crud/templates/drizzle-table.ts.template +12 -0
- package/dist/generators/crud/templates/handlers.ts.template +136 -0
- package/dist/generators/crud/templates/routes.ts.template +21 -0
- package/dist/generators/crud/templates/schema.ts.template +20 -0
- package/dist/index.js +9618 -0
- package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/index.ts.template +79 -0
- package/dist/integrations/analytics/providers/amplitude/templates/src/analytics/types.ts.template +12 -0
- package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/index.ts.template +62 -0
- package/dist/integrations/analytics/providers/mixpanel/templates/src/analytics/types.ts.template +12 -0
- package/dist/integrations/analytics/providers/posthog/templates/src/analytics/index.ts.template +67 -0
- package/dist/integrations/analytics/providers/posthog/templates/src/analytics/types.ts.template +12 -0
- package/dist/integrations/auth/providers/authjoy/templates/api/src/middleware/auth.ts.template +89 -0
- package/dist/integrations/auth/providers/authjoy/templates/api/src/routes/auth.ts.template +27 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/components/AuthProvider.tsx.template +40 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/hooks/use-auth.ts.template +71 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/lib/auth.ts.template +59 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/account.tsx.template +84 -0
- package/dist/integrations/auth/providers/authjoy/templates/web/src/pages/login.tsx.template +73 -0
- package/dist/integrations/cache/providers/memory/templates/src/cache/index.ts.template +43 -0
- package/dist/integrations/cache/providers/memory/templates/src/cache/types.ts.template +3 -0
- package/dist/integrations/cache/providers/redis/templates/src/cache/index.ts.template +37 -0
- package/dist/integrations/cache/providers/redis/templates/src/cache/types.ts.template +3 -0
- package/dist/integrations/cache/providers/valkey/templates/src/cache/index.ts.template +38 -0
- package/dist/integrations/cache/providers/valkey/templates/src/cache/types.ts.template +3 -0
- package/dist/integrations/db/providers/postgres/templates/drizzle.config.ts.template +10 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/index.ts.template +13 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/migrate.ts.template +19 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/postgres/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/db/providers/sqlite/templates/drizzle.config.ts.template +10 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/index.ts.template +10 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/sqlite/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/db/providers/supabase/templates/drizzle.config.ts.template +10 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/index.ts.template +13 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/migrate.ts.template +19 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/supabase/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/db/providers/turso/templates/drizzle.config.ts.template +11 -0
- package/dist/integrations/db/providers/turso/templates/src/db/index.ts.template +14 -0
- package/dist/integrations/db/providers/turso/templates/src/db/schema/index.ts.template +1 -0
- package/dist/integrations/db/providers/turso/templates/src/db/schema/users.ts.template +12 -0
- package/dist/integrations/db/providers/turso/templates/src/db/seed.ts.template +28 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/index.ts.template +24 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/index.ts.template +1 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/templates/welcome.ts.template +7 -0
- package/dist/integrations/email/providers/nodemailer/templates/src/email/types.ts.template +7 -0
- package/dist/integrations/email/providers/resend/templates/src/email/index.ts.template +18 -0
- package/dist/integrations/email/providers/resend/templates/src/email/templates/index.ts.template +1 -0
- package/dist/integrations/email/providers/resend/templates/src/email/templates/welcome.ts.template +7 -0
- package/dist/integrations/email/providers/resend/templates/src/email/types.ts.template +7 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/index.ts.template +16 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/index.ts.template +1 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/templates/welcome.ts.template +7 -0
- package/dist/integrations/email/providers/sendgrid/templates/src/email/types.ts.template +7 -0
- package/dist/integrations/flags/providers/local/templates/api/src/lib/flags.ts.template +97 -0
- package/dist/integrations/flags/providers/local/templates/api/src/routes/flags.ts.template +36 -0
- package/dist/integrations/flags/providers/local/templates/flags.json.template +8 -0
- package/dist/integrations/flags/providers/local/templates/web/src/hooks/use-flag.ts.template +60 -0
- package/dist/integrations/logging/providers/axiom/templates/src/logging/index.ts.template +56 -0
- package/dist/integrations/logging/providers/axiom/templates/src/logging/types.ts.template +5 -0
- package/dist/integrations/logging/providers/pino/templates/src/logging/index.ts.template +21 -0
- package/dist/integrations/logging/providers/pino/templates/src/logging/types.ts.template +5 -0
- package/dist/integrations/logging/providers/winston/templates/src/logging/index.ts.template +30 -0
- package/dist/integrations/logging/providers/winston/templates/src/logging/types.ts.template +5 -0
- package/dist/integrations/monitor/providers/datadog/templates/src/monitor/index.ts.template +78 -0
- package/dist/integrations/monitor/providers/datadog/templates/src/monitor/types.ts.template +12 -0
- package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/index.ts.template +60 -0
- package/dist/integrations/monitor/providers/newrelic/templates/src/monitor/types.ts.template +12 -0
- package/dist/integrations/monitor/providers/sentry/templates/src/monitor/index.ts.template +70 -0
- package/dist/integrations/monitor/providers/sentry/templates/src/monitor/types.ts.template +12 -0
- package/dist/integrations/queue/providers/bullmq/templates/src/queue/index.ts.template +56 -0
- package/dist/integrations/queue/providers/bullmq/templates/src/queue/types.ts.template +10 -0
- package/dist/integrations/queue/providers/memory/templates/src/queue/index.ts.template +73 -0
- package/dist/integrations/queue/providers/memory/templates/src/queue/types.ts.template +10 -0
- package/dist/integrations/queue/providers/pgboss/templates/src/queue/index.ts.template +34 -0
- package/dist/integrations/queue/providers/pgboss/templates/src/queue/types.ts.template +10 -0
- package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/index.ts.template +95 -0
- package/dist/integrations/ratelimit/providers/memory/templates/src/ratelimit/types.ts.template +12 -0
- package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/index.ts.template +80 -0
- package/dist/integrations/ratelimit/providers/rate-limiter-flexible/templates/src/ratelimit/types.ts.template +12 -0
- package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/index.ts.template +67 -0
- package/dist/integrations/ratelimit/providers/upstash/templates/src/ratelimit/types.ts.template +12 -0
- package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/index.ts.template +81 -0
- package/dist/integrations/schedule/providers/bullmq/templates/src/schedule/types.ts.template +10 -0
- package/dist/integrations/schedule/providers/croner/templates/src/schedule/index.ts.template +47 -0
- package/dist/integrations/schedule/providers/croner/templates/src/schedule/types.ts.template +10 -0
- package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/index.ts.template +45 -0
- package/dist/integrations/schedule/providers/node-cron/templates/src/schedule/types.ts.template +10 -0
- package/dist/integrations/search/providers/algolia/templates/src/search/index.ts.template +52 -0
- package/dist/integrations/search/providers/algolia/templates/src/search/types.ts.template +18 -0
- package/dist/integrations/search/providers/meilisearch/templates/src/search/index.ts.template +49 -0
- package/dist/integrations/search/providers/meilisearch/templates/src/search/types.ts.template +18 -0
- package/dist/integrations/search/providers/typesense/templates/src/search/index.ts.template +71 -0
- package/dist/integrations/search/providers/typesense/templates/src/search/types.ts.template +35 -0
- package/dist/integrations/storage/providers/local/templates/src/storage/index.ts.template +69 -0
- package/dist/integrations/storage/providers/local/templates/src/storage/types.ts.template +12 -0
- package/dist/integrations/storage/providers/r2/templates/src/storage/index.ts.template +80 -0
- package/dist/integrations/storage/providers/r2/templates/src/storage/types.ts.template +12 -0
- package/dist/integrations/storage/providers/s3/templates/src/storage/index.ts.template +78 -0
- package/dist/integrations/storage/providers/s3/templates/src/storage/types.ts.template +12 -0
- package/dist/integrations/stripe/templates/api/src/lib/stripe.ts.template +259 -0
- package/dist/integrations/stripe/templates/api/src/routes/stripe-webhooks.ts.template +284 -0
- package/dist/integrations/stripe/templates/api/stripe.config.ts.template +178 -0
- package/dist/integrations/stripe/templates/shared/src/pricing.ts.template +117 -0
- package/dist/integrations/stripe/templates/shared/src/stripe-types.ts.template +133 -0
- package/dist/integrations/stripe/templates/web/src/components/billing-settings.tsx.template +123 -0
- package/dist/integrations/stripe/templates/web/src/components/pricing-cards.tsx.template +115 -0
- package/dist/integrations/stripe/templates/web/src/pages/pricing.tsx.template +95 -0
- package/dist/templates/api/fastify/.env.example.template +7 -0
- package/dist/templates/api/fastify/.gitignore.template +24 -0
- package/dist/templates/api/fastify/package.json.template +23 -0
- package/dist/templates/api/fastify/src/index.ts.template +52 -0
- package/dist/templates/api/fastify/src/lib/env.ts.template +20 -0
- package/dist/templates/api/fastify/src/routes/health.ts.template +12 -0
- package/dist/templates/api/fastify/tsconfig.json.template +18 -0
- package/dist/templates/api/fastify-postgres/.env.example.template +10 -0
- package/dist/templates/api/fastify-postgres/drizzle.config.ts.template +10 -0
- package/dist/templates/api/fastify-postgres/package.json.template +16 -0
- package/dist/templates/api/fastify-postgres/src/db/index.ts.template +9 -0
- package/dist/templates/api/fastify-postgres/src/db/schema/users.ts.template +12 -0
- package/dist/templates/api/fastify-sqlite/.env.example.template +10 -0
- package/dist/templates/api/fastify-sqlite/drizzle.config.ts.template +10 -0
- package/dist/templates/api/fastify-sqlite/package.json.template +16 -0
- package/dist/templates/api/fastify-sqlite/src/db/index.ts.template +6 -0
- package/dist/templates/api/fastify-sqlite/src/db/schema/users.ts.template +12 -0
- package/dist/templates/api/fastify-supabase/.env.example.template +10 -0
- package/dist/templates/api/fastify-supabase/drizzle.config.ts.template +10 -0
- package/dist/templates/api/fastify-supabase/package.json.template +15 -0
- package/dist/templates/api/fastify-supabase/src/db/index.ts.template +9 -0
- package/dist/templates/api/fastify-supabase/src/db/schema/users.ts.template +12 -0
- package/dist/templates/api/fastify-turso/.env.example.template +11 -0
- package/dist/templates/api/fastify-turso/drizzle.config.ts.template +11 -0
- package/dist/templates/api/fastify-turso/package.json.template +15 -0
- package/dist/templates/api/fastify-turso/src/db/index.ts.template +10 -0
- package/dist/templates/api/fastify-turso/src/db/schema/users.ts.template +12 -0
- package/dist/templates/fullstack/api/.env.example.template +10 -0
- package/dist/templates/fullstack/api/.gitignore.template +4 -0
- package/dist/templates/fullstack/api/drizzle.config.ts.template +14 -0
- package/dist/templates/fullstack/api/package.json.template +33 -0
- package/dist/templates/fullstack/api/src/db/index.ts.template +13 -0
- package/dist/templates/fullstack/api/src/db/schema/api-keys.ts.template +19 -0
- package/dist/templates/fullstack/api/src/db/schema/audit-logs.ts.template +23 -0
- package/dist/templates/fullstack/api/src/db/schema/index.ts.template +8 -0
- package/dist/templates/fullstack/api/src/db/schema/invites.ts.template +19 -0
- package/dist/templates/fullstack/api/src/db/schema/memberships.ts.template +16 -0
- package/dist/templates/fullstack/api/src/db/schema/organizations.ts.template +13 -0
- package/dist/templates/fullstack/api/src/db/schema/plans.ts.template +29 -0
- package/dist/templates/fullstack/api/src/db/schema/subscriptions.ts.template +38 -0
- package/dist/templates/fullstack/api/src/db/schema/users.ts.template +14 -0
- package/dist/templates/fullstack/api/src/index.ts.template +54 -0
- package/dist/templates/fullstack/api/src/lib/env.ts.template +22 -0
- package/dist/templates/fullstack/api/src/routes/health.ts.template +14 -0
- package/dist/templates/fullstack/api/tsconfig.json.template +15 -0
- package/dist/templates/fullstack/root/.gitignore.template +26 -0
- package/dist/templates/fullstack/root/package.json.template +15 -0
- package/dist/templates/fullstack/root/pnpm-workspace.yaml.template +3 -0
- package/dist/templates/fullstack/root/turbo.json.template +17 -0
- package/dist/templates/fullstack/shared/package.json.template +36 -0
- package/dist/templates/fullstack/shared/src/index.ts.template +8 -0
- package/dist/templates/fullstack/shared/src/schemas/api-key.ts.template +28 -0
- package/dist/templates/fullstack/shared/src/schemas/audit-log.ts.template +41 -0
- package/dist/templates/fullstack/shared/src/schemas/index.ts.template +8 -0
- package/dist/templates/fullstack/shared/src/schemas/invite.ts.template +25 -0
- package/dist/templates/fullstack/shared/src/schemas/membership.ts.template +20 -0
- package/dist/templates/fullstack/shared/src/schemas/organization.ts.template +18 -0
- package/dist/templates/fullstack/shared/src/schemas/plan.ts.template +38 -0
- package/dist/templates/fullstack/shared/src/schemas/subscription.ts.template +56 -0
- package/dist/templates/fullstack/shared/src/schemas/user.ts.template +21 -0
- package/dist/templates/fullstack/shared/src/types/index.ts.template +75 -0
- package/dist/templates/fullstack/shared/src/validators/index.ts.template +53 -0
- package/dist/templates/fullstack/shared/tsconfig.json.template +17 -0
- package/dist/templates/fullstack/web/.gitignore.template +3 -0
- package/dist/templates/fullstack/web/index.html.template +13 -0
- package/dist/templates/fullstack/web/package.json.template +23 -0
- package/dist/templates/fullstack/web/src/App.tsx.template +47 -0
- package/dist/templates/fullstack/web/src/index.css.template +54 -0
- package/dist/templates/fullstack/web/src/main.tsx.template +10 -0
- package/dist/templates/fullstack/web/src/vite-env.d.ts.template +1 -0
- package/dist/templates/fullstack/web/tsconfig.json.template +21 -0
- package/dist/templates/fullstack/web/tsconfig.node.json.template +11 -0
- package/dist/templates/fullstack/web/vite.config.ts.template +15 -0
- package/dist/templates/hosted/root/.env.local.template +13 -0
- package/dist/templates/hosted/root/.gitignore.template +32 -0
- package/dist/templates/hosted/root/CLAUDE.md.template +139 -0
- package/dist/templates/hosted/root/drizzle.config.ts.template +10 -0
- package/dist/templates/hosted/root/next.config.ts.template +15 -0
- package/dist/templates/hosted/root/package.json.template +40 -0
- package/dist/templates/hosted/root/postcss.config.mjs.template +9 -0
- package/dist/templates/hosted/root/primstack.config.json.template +5 -0
- package/dist/templates/hosted/root/tailwind.config.ts.template +14 -0
- package/dist/templates/hosted/root/tsconfig.json.template +25 -0
- package/dist/templates/hosted/root/wrangler.toml.template +9 -0
- package/dist/templates/hosted/src/app/actions/example.ts.template +50 -0
- package/dist/templates/hosted/src/app/api/health/route.ts.template +5 -0
- package/dist/templates/hosted/src/app/auth/login/page.tsx.template +32 -0
- package/dist/templates/hosted/src/app/globals.css.template +59 -0
- package/dist/templates/hosted/src/app/layout.tsx.template +24 -0
- package/dist/templates/hosted/src/app/page.tsx.template +34 -0
- package/dist/templates/hosted/src/db/migrations/0000_initial.sql.template +43 -0
- package/dist/templates/hosted/src/db/schema.ts.template +52 -0
- package/dist/templates/hosted/src/env.d.ts.template +10 -0
- package/dist/templates/hosted/src/instrumentation.ts.template +6 -0
- package/dist/templates/hosted/src/lib/auth.ts.template +35 -0
- package/dist/templates/hosted/src/lib/db.ts.template +17 -0
- package/dist/templates/hosted/src/middleware.ts.template +6 -0
- package/package.json +46 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { MeiliSearch } from 'meilisearch';
|
|
2
|
+
import type { SearchDocument, SearchOptions, SearchResult } from './types.js';
|
|
3
|
+
|
|
4
|
+
export const client = new MeiliSearch({
|
|
5
|
+
host: process.env.MEILISEARCH_URL || 'http://localhost:7700',
|
|
6
|
+
apiKey: process.env.MEILISEARCH_API_KEY || '',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function getIndex(name: string) {
|
|
10
|
+
return client.index(name);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function addDocuments<T extends SearchDocument>(
|
|
14
|
+
indexName: string,
|
|
15
|
+
documents: T[],
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const index = client.index(indexName);
|
|
18
|
+
await index.addDocuments(documents);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function search<T extends SearchDocument>(
|
|
22
|
+
indexName: string,
|
|
23
|
+
query: string,
|
|
24
|
+
options?: SearchOptions,
|
|
25
|
+
): Promise<SearchResult<T>> {
|
|
26
|
+
const index = client.index(indexName);
|
|
27
|
+
const result = await index.search(query, {
|
|
28
|
+
limit: options?.limit ?? 20,
|
|
29
|
+
offset: options?.offset,
|
|
30
|
+
filter: options?.filter,
|
|
31
|
+
sort: options?.sort,
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
hits: result.hits as T[],
|
|
35
|
+
totalHits: result.estimatedTotalHits ?? result.hits.length,
|
|
36
|
+
query,
|
|
37
|
+
processingTimeMs: result.processingTimeMs,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function deleteDocuments(
|
|
42
|
+
indexName: string,
|
|
43
|
+
ids: (string | number)[],
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const index = client.index(indexName);
|
|
46
|
+
await index.deleteDocuments(ids);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import Typesense from 'typesense';
|
|
2
|
+
import type { SearchDocument, SearchOptions, SearchResult, CollectionSchema } from './types.js';
|
|
3
|
+
|
|
4
|
+
const url = new URL(process.env.TYPESENSE_URL || 'http://localhost:8108');
|
|
5
|
+
|
|
6
|
+
export const client = new Typesense.Client({
|
|
7
|
+
nodes: [
|
|
8
|
+
{
|
|
9
|
+
host: url.hostname,
|
|
10
|
+
port: Number(url.port) || 8108,
|
|
11
|
+
protocol: url.protocol.replace(':', ''),
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
apiKey: process.env.TYPESENSE_API_KEY || '',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function getCollection(name: string) {
|
|
18
|
+
return client.collections(name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a collection if it doesn't already exist.
|
|
23
|
+
* Must be called before addDocuments for new collections.
|
|
24
|
+
*/
|
|
25
|
+
export async function ensureCollection(schema: CollectionSchema): Promise<void> {
|
|
26
|
+
try {
|
|
27
|
+
await client.collections(schema.name).retrieve();
|
|
28
|
+
} catch {
|
|
29
|
+
await client.collections().create(schema);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function addDocuments<T extends SearchDocument>(
|
|
34
|
+
collectionName: string,
|
|
35
|
+
documents: T[],
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
await client.collections(collectionName).documents().import(documents, { action: 'upsert' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function search<T extends SearchDocument>(
|
|
41
|
+
collectionName: string,
|
|
42
|
+
query: string,
|
|
43
|
+
options?: SearchOptions,
|
|
44
|
+
): Promise<SearchResult<T>> {
|
|
45
|
+
const start = Date.now();
|
|
46
|
+
const result = await client.collections(collectionName).documents().search({
|
|
47
|
+
q: query,
|
|
48
|
+
query_by: options?.queryBy || 'title',
|
|
49
|
+
per_page: options?.limit ?? 20,
|
|
50
|
+
page: options?.offset ? Math.floor(options.offset / (options?.limit ?? 20)) + 1 : 1,
|
|
51
|
+
filter_by: options?.filter,
|
|
52
|
+
sort_by: options?.sort?.join(','),
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
hits: (result.hits ?? []).map((h) => h.document as T),
|
|
56
|
+
totalHits: result.found ?? 0,
|
|
57
|
+
query,
|
|
58
|
+
processingTimeMs: Date.now() - start,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function deleteDocuments(
|
|
63
|
+
collectionName: string,
|
|
64
|
+
ids: (string | number)[],
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
await Promise.all(
|
|
67
|
+
ids.map((id) => client.collections(collectionName).documents(String(id)).delete()),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type { SearchDocument, SearchOptions, SearchResult, CollectionSchema } from './types.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
/** Comma-separated field names to search (required for Typesense, e.g. 'title,body') */
|
|
12
|
+
queryBy?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SearchResult<T = SearchDocument> {
|
|
16
|
+
hits: T[];
|
|
17
|
+
totalHits: number;
|
|
18
|
+
query: string;
|
|
19
|
+
processingTimeMs: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CollectionSchema {
|
|
23
|
+
name: string;
|
|
24
|
+
fields: CollectionField[];
|
|
25
|
+
default_sorting_field?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CollectionField {
|
|
29
|
+
name: string;
|
|
30
|
+
type: 'string' | 'int32' | 'int64' | 'float' | 'bool' | 'string[]' | 'int32[]' | 'int64[]' | 'float[]' | 'bool[]' | 'auto';
|
|
31
|
+
facet?: boolean;
|
|
32
|
+
optional?: boolean;
|
|
33
|
+
index?: boolean;
|
|
34
|
+
sort?: boolean;
|
|
35
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import type { UploadOptions, StorageObject } from './types.js';
|
|
4
|
+
|
|
5
|
+
const STORAGE_ROOT = process.env.STORAGE_PATH || './uploads';
|
|
6
|
+
|
|
7
|
+
function resolve(bucket?: string, key?: string): string {
|
|
8
|
+
const parts = [STORAGE_ROOT];
|
|
9
|
+
if (bucket) parts.push(bucket);
|
|
10
|
+
if (key) parts.push(key);
|
|
11
|
+
const resolved = path.resolve(...parts);
|
|
12
|
+
const root = path.resolve(STORAGE_ROOT);
|
|
13
|
+
if (resolved !== root && !resolved.startsWith(root + path.sep)) {
|
|
14
|
+
throw new Error('Path traversal detected');
|
|
15
|
+
}
|
|
16
|
+
return resolved;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function uploadFile(options: UploadOptions): Promise<{ key: string }> {
|
|
20
|
+
const filePath = resolve(options.bucket, options.key);
|
|
21
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
22
|
+
await fs.writeFile(filePath, options.body);
|
|
23
|
+
return { key: options.key };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function downloadFile(key: string, bucket?: string): Promise<Buffer> {
|
|
27
|
+
const filePath = resolve(bucket, key);
|
|
28
|
+
return fs.readFile(filePath);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function deleteFile(key: string, bucket?: string): Promise<void> {
|
|
32
|
+
const filePath = resolve(bucket, key);
|
|
33
|
+
await fs.remove(filePath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function listFiles(prefix?: string, bucket?: string): Promise<StorageObject[]> {
|
|
37
|
+
const dir = resolve(bucket);
|
|
38
|
+
if (!(await fs.pathExists(dir))) return [];
|
|
39
|
+
|
|
40
|
+
const entries = await fs.readdir(dir);
|
|
41
|
+
const results: StorageObject[] = [];
|
|
42
|
+
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (prefix && !entry.startsWith(prefix)) continue;
|
|
45
|
+
const filePath = path.join(dir, entry);
|
|
46
|
+
const stat = await fs.stat(filePath);
|
|
47
|
+
if (stat.isFile()) {
|
|
48
|
+
results.push({
|
|
49
|
+
key: entry,
|
|
50
|
+
size: stat.size,
|
|
51
|
+
lastModified: stat.mtime,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function listBuckets(): Promise<string[]> {
|
|
60
|
+
await fs.ensureDir(STORAGE_ROOT);
|
|
61
|
+
const entries = await fs.readdir(STORAGE_ROOT, { withFileTypes: true });
|
|
62
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function createBucket(name: string): Promise<void> {
|
|
66
|
+
await fs.ensureDir(resolve(name));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type { UploadOptions, StorageObject } from './types.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
S3Client,
|
|
3
|
+
PutObjectCommand,
|
|
4
|
+
GetObjectCommand,
|
|
5
|
+
DeleteObjectCommand,
|
|
6
|
+
ListObjectsV2Command,
|
|
7
|
+
} from '@aws-sdk/client-s3';
|
|
8
|
+
import type { UploadOptions, StorageObject } from './types.js';
|
|
9
|
+
|
|
10
|
+
const s3 = new S3Client({
|
|
11
|
+
region: 'auto',
|
|
12
|
+
endpoint: `https://${process.env.R2_ACCOUNT_ID!}.r2.cloudflarestorage.com`,
|
|
13
|
+
credentials: {
|
|
14
|
+
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
|
15
|
+
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const DEFAULT_BUCKET = process.env.R2_BUCKET!;
|
|
20
|
+
|
|
21
|
+
export async function uploadFile(options: UploadOptions): Promise<{ key: string }> {
|
|
22
|
+
const bucket = options.bucket || DEFAULT_BUCKET;
|
|
23
|
+
await s3.send(
|
|
24
|
+
new PutObjectCommand({
|
|
25
|
+
Bucket: bucket,
|
|
26
|
+
Key: options.key,
|
|
27
|
+
Body: options.body,
|
|
28
|
+
ContentType: options.contentType,
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
return { key: options.key };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function downloadFile(key: string, bucket?: string): Promise<Buffer> {
|
|
35
|
+
const response = await s3.send(
|
|
36
|
+
new GetObjectCommand({
|
|
37
|
+
Bucket: bucket || DEFAULT_BUCKET,
|
|
38
|
+
Key: key,
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
const bytes = await response.Body!.transformToByteArray();
|
|
42
|
+
return Buffer.from(bytes);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function deleteFile(key: string, bucket?: string): Promise<void> {
|
|
46
|
+
await s3.send(
|
|
47
|
+
new DeleteObjectCommand({
|
|
48
|
+
Bucket: bucket || DEFAULT_BUCKET,
|
|
49
|
+
Key: key,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function listFiles(prefix?: string, bucket?: string): Promise<StorageObject[]> {
|
|
55
|
+
const response = await s3.send(
|
|
56
|
+
new ListObjectsV2Command({
|
|
57
|
+
Bucket: bucket || DEFAULT_BUCKET,
|
|
58
|
+
Prefix: prefix,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
return (response.Contents || []).map((obj) => ({
|
|
62
|
+
key: obj.Key!,
|
|
63
|
+
size: obj.Size || 0,
|
|
64
|
+
lastModified: obj.LastModified || new Date(),
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function listBuckets(): Promise<string[]> {
|
|
69
|
+
throw new Error(
|
|
70
|
+
'R2 does not support ListBuckets via the S3 API. Use the Cloudflare dashboard or wrangler CLI instead.',
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function createBucket(name: string): Promise<void> {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`R2 does not support CreateBucket via the S3 API. Use the Cloudflare dashboard or 'wrangler r2 bucket create ${name}'.`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type { UploadOptions, StorageObject } from './types.js';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
S3Client,
|
|
3
|
+
PutObjectCommand,
|
|
4
|
+
GetObjectCommand,
|
|
5
|
+
DeleteObjectCommand,
|
|
6
|
+
ListObjectsV2Command,
|
|
7
|
+
ListBucketsCommand,
|
|
8
|
+
CreateBucketCommand,
|
|
9
|
+
} from '@aws-sdk/client-s3';
|
|
10
|
+
import type { UploadOptions, StorageObject } from './types.js';
|
|
11
|
+
|
|
12
|
+
const s3 = new S3Client({
|
|
13
|
+
region: process.env.AWS_REGION || 'us-east-1',
|
|
14
|
+
credentials: {
|
|
15
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
16
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const DEFAULT_BUCKET = process.env.S3_BUCKET!;
|
|
21
|
+
|
|
22
|
+
export async function uploadFile(options: UploadOptions): Promise<{ key: string }> {
|
|
23
|
+
const bucket = options.bucket || DEFAULT_BUCKET;
|
|
24
|
+
await s3.send(
|
|
25
|
+
new PutObjectCommand({
|
|
26
|
+
Bucket: bucket,
|
|
27
|
+
Key: options.key,
|
|
28
|
+
Body: options.body,
|
|
29
|
+
ContentType: options.contentType,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
return { key: options.key };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function downloadFile(key: string, bucket?: string): Promise<Buffer> {
|
|
36
|
+
const response = await s3.send(
|
|
37
|
+
new GetObjectCommand({
|
|
38
|
+
Bucket: bucket || DEFAULT_BUCKET,
|
|
39
|
+
Key: key,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
const bytes = await response.Body!.transformToByteArray();
|
|
43
|
+
return Buffer.from(bytes);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function deleteFile(key: string, bucket?: string): Promise<void> {
|
|
47
|
+
await s3.send(
|
|
48
|
+
new DeleteObjectCommand({
|
|
49
|
+
Bucket: bucket || DEFAULT_BUCKET,
|
|
50
|
+
Key: key,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function listFiles(prefix?: string, bucket?: string): Promise<StorageObject[]> {
|
|
56
|
+
const response = await s3.send(
|
|
57
|
+
new ListObjectsV2Command({
|
|
58
|
+
Bucket: bucket || DEFAULT_BUCKET,
|
|
59
|
+
Prefix: prefix,
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
return (response.Contents || []).map((obj) => ({
|
|
63
|
+
key: obj.Key!,
|
|
64
|
+
size: obj.Size || 0,
|
|
65
|
+
lastModified: obj.LastModified || new Date(),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function listBuckets(): Promise<string[]> {
|
|
70
|
+
const response = await s3.send(new ListBucketsCommand({}));
|
|
71
|
+
return (response.Buckets || []).map((b) => b.Name!);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function createBucket(name: string): Promise<void> {
|
|
75
|
+
await s3.send(new CreateBucketCommand({ Bucket: name }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type { UploadOptions, StorageObject } from './types.js';
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe Client
|
|
3
|
+
*
|
|
4
|
+
* This module provides a configured Stripe client and helper functions
|
|
5
|
+
* for common billing operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Stripe from 'stripe';
|
|
9
|
+
import { stripeConfig } from '../../stripe.config.js';
|
|
10
|
+
|
|
11
|
+
type StripeEnvironment = 'test' | 'live';
|
|
12
|
+
|
|
13
|
+
// Get current environment
|
|
14
|
+
const STRIPE_ENV = (process.env.STRIPE_ENV || 'test') as StripeEnvironment;
|
|
15
|
+
|
|
16
|
+
// Initialize Stripe client
|
|
17
|
+
const credentials = stripeConfig.environments[STRIPE_ENV];
|
|
18
|
+
|
|
19
|
+
if (!credentials?.secretKey) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Stripe ${STRIPE_ENV} secret key not configured. ` +
|
|
22
|
+
`Set STRIPE_${STRIPE_ENV.toUpperCase()}_SECRET_KEY environment variable.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const stripe = new Stripe(credentials.secretKey, {
|
|
27
|
+
apiVersion: '2024-06-20' as any,
|
|
28
|
+
typescript: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the current Stripe environment
|
|
33
|
+
*/
|
|
34
|
+
export function getStripeEnvironment(): StripeEnvironment {
|
|
35
|
+
return STRIPE_ENV;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the publishable key for the current environment
|
|
40
|
+
*/
|
|
41
|
+
export function getPublishableKey(): string {
|
|
42
|
+
return credentials.publishableKey;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the webhook secret for the current environment
|
|
47
|
+
*/
|
|
48
|
+
export function getWebhookSecret(): string {
|
|
49
|
+
return credentials.webhookSecret;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get or create a Stripe customer for a user
|
|
54
|
+
*
|
|
55
|
+
* IMPORTANT: This function searches by userId metadata to avoid
|
|
56
|
+
* matching wrong customers when emails are shared or reused.
|
|
57
|
+
*/
|
|
58
|
+
export async function getOrCreateCustomer(
|
|
59
|
+
userId: string,
|
|
60
|
+
email: string,
|
|
61
|
+
name?: string
|
|
62
|
+
): Promise<Stripe.Customer> {
|
|
63
|
+
// First, search for existing customer by userId metadata
|
|
64
|
+
// This is the primary lookup to ensure we don't mix up customers
|
|
65
|
+
const customersByUserId = await stripe.customers.search({
|
|
66
|
+
query: `metadata['userId']:'${userId}'`,
|
|
67
|
+
limit: 1,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (customersByUserId.data.length > 0) {
|
|
71
|
+
const customer = customersByUserId.data[0];
|
|
72
|
+
// Update email if it changed (but keep userId as-is)
|
|
73
|
+
if (customer.email !== email) {
|
|
74
|
+
return stripe.customers.update(customer.id, { email });
|
|
75
|
+
}
|
|
76
|
+
return customer;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// No customer found by userId - create a new one
|
|
80
|
+
// Do NOT associate existing email-only customers to avoid billing mix-ups
|
|
81
|
+
return stripe.customers.create({
|
|
82
|
+
email,
|
|
83
|
+
name,
|
|
84
|
+
metadata: {
|
|
85
|
+
userId,
|
|
86
|
+
environment: STRIPE_ENV,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a checkout session for a subscription
|
|
93
|
+
*/
|
|
94
|
+
export async function createCheckoutSession(options: {
|
|
95
|
+
customerId: string;
|
|
96
|
+
priceLookupKey: string;
|
|
97
|
+
successUrl: string;
|
|
98
|
+
cancelUrl: string;
|
|
99
|
+
trialDays?: number;
|
|
100
|
+
metadata?: Record<string, string>;
|
|
101
|
+
}): Promise<Stripe.Checkout.Session> {
|
|
102
|
+
const { customerId, priceLookupKey, successUrl, cancelUrl, trialDays, metadata } = options;
|
|
103
|
+
|
|
104
|
+
// Get price by lookup key
|
|
105
|
+
const prices = await stripe.prices.list({
|
|
106
|
+
lookup_keys: [priceLookupKey],
|
|
107
|
+
limit: 1,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (prices.data.length === 0) {
|
|
111
|
+
throw new Error(`Price not found for lookup key: ${priceLookupKey}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const price = prices.data[0];
|
|
115
|
+
|
|
116
|
+
return stripe.checkout.sessions.create({
|
|
117
|
+
customer: customerId,
|
|
118
|
+
mode: 'subscription',
|
|
119
|
+
line_items: [
|
|
120
|
+
{
|
|
121
|
+
price: price.id,
|
|
122
|
+
quantity: 1,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
success_url: successUrl,
|
|
126
|
+
cancel_url: cancelUrl,
|
|
127
|
+
subscription_data: trialDays
|
|
128
|
+
? {
|
|
129
|
+
trial_period_days: trialDays,
|
|
130
|
+
metadata,
|
|
131
|
+
}
|
|
132
|
+
: { metadata },
|
|
133
|
+
metadata,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create a customer portal session
|
|
139
|
+
*/
|
|
140
|
+
export async function createPortalSession(
|
|
141
|
+
customerId: string,
|
|
142
|
+
returnUrl: string
|
|
143
|
+
): Promise<Stripe.BillingPortal.Session> {
|
|
144
|
+
return stripe.billingPortal.sessions.create({
|
|
145
|
+
customer: customerId,
|
|
146
|
+
return_url: returnUrl,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get a subscription by ID
|
|
152
|
+
*/
|
|
153
|
+
export async function getSubscription(
|
|
154
|
+
subscriptionId: string
|
|
155
|
+
): Promise<Stripe.Subscription | null> {
|
|
156
|
+
try {
|
|
157
|
+
return await stripe.subscriptions.retrieve(subscriptionId);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
// Handle Stripe API errors - check for resource not found
|
|
160
|
+
const stripeError = error as Stripe.errors.StripeError;
|
|
161
|
+
if (stripeError.type === 'invalid_request_error' && stripeError.code === 'resource_missing') {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Cancel a subscription
|
|
170
|
+
*/
|
|
171
|
+
export async function cancelSubscription(
|
|
172
|
+
subscriptionId: string,
|
|
173
|
+
immediately = false
|
|
174
|
+
): Promise<Stripe.Subscription> {
|
|
175
|
+
if (immediately) {
|
|
176
|
+
return stripe.subscriptions.cancel(subscriptionId);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Cancel at period end
|
|
180
|
+
return stripe.subscriptions.update(subscriptionId, {
|
|
181
|
+
cancel_at_period_end: true,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Update subscription to a new price
|
|
187
|
+
*
|
|
188
|
+
* Note: This updates the first subscription item (the main plan).
|
|
189
|
+
* If the subscription has multiple items (add-ons, metered), they remain unchanged.
|
|
190
|
+
*/
|
|
191
|
+
export async function updateSubscription(
|
|
192
|
+
subscriptionId: string,
|
|
193
|
+
newPriceLookupKey: string
|
|
194
|
+
): Promise<Stripe.Subscription> {
|
|
195
|
+
// Get the subscription
|
|
196
|
+
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
197
|
+
|
|
198
|
+
// Validate subscription has at least one item
|
|
199
|
+
const firstItem = subscription.items.data[0];
|
|
200
|
+
if (!firstItem) {
|
|
201
|
+
throw new Error(`Subscription ${subscriptionId} has no items to update`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get new price by lookup key
|
|
205
|
+
const prices = await stripe.prices.list({
|
|
206
|
+
lookup_keys: [newPriceLookupKey],
|
|
207
|
+
limit: 1,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (prices.data.length === 0) {
|
|
211
|
+
throw new Error(`Price not found for lookup key: ${newPriceLookupKey}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const newPrice = prices.data[0];
|
|
215
|
+
|
|
216
|
+
// Update only the first subscription item (main plan)
|
|
217
|
+
return stripe.subscriptions.update(subscriptionId, {
|
|
218
|
+
items: [
|
|
219
|
+
{
|
|
220
|
+
id: firstItem.id,
|
|
221
|
+
price: newPrice.id,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
proration_behavior: 'create_prorations',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Report usage for a meter
|
|
230
|
+
*/
|
|
231
|
+
export async function reportUsage(
|
|
232
|
+
subscriptionItemId: string,
|
|
233
|
+
quantity: number,
|
|
234
|
+
timestamp?: number
|
|
235
|
+
): Promise<Stripe.UsageRecord> {
|
|
236
|
+
return stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
|
|
237
|
+
quantity,
|
|
238
|
+
timestamp: timestamp || Math.floor(Date.now() / 1000),
|
|
239
|
+
action: 'increment',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Construct and verify a webhook event
|
|
245
|
+
*/
|
|
246
|
+
export function constructWebhookEvent(
|
|
247
|
+
payload: string | Buffer,
|
|
248
|
+
signature: string
|
|
249
|
+
): Stripe.Event {
|
|
250
|
+
const webhookSecret = getWebhookSecret();
|
|
251
|
+
|
|
252
|
+
if (!webhookSecret) {
|
|
253
|
+
throw new Error('Webhook secret not configured');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return stripe.webhooks.constructEvent(payload, signature, webhookSecret);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export default stripe;
|