spfn 0.2.0-beta.46 → 0.2.0-beta.48

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.
@@ -14,6 +14,7 @@ dist
14
14
  .env
15
15
  .env.local
16
16
  .env*.local
17
+ .env.server
17
18
 
18
19
  # Git
19
20
  .git
@@ -1,5 +1,5 @@
1
1
  # Production Dockerfile for SPFN
2
- FROM node:20-alpine
2
+ FROM node:22-alpine
3
3
 
4
4
  WORKDIR /app
5
5
 
@@ -36,88 +36,23 @@ export const envSchema = defineEnvSchema({
36
36
  });
37
37
 
38
38
  /**
39
- * Type-safe Environment Registry
39
+ * Type-safe Environment Variables
40
40
  *
41
- * Use this registry to access environment variables throughout your application.
42
- * It provides type-safe access and automatic validation.
41
+ * Variables are lazily validated on access — if a required variable is
42
+ * missing or invalid, an error is thrown at the first access point.
43
+ * This is timing-safe with dotenv loading.
43
44
  *
44
45
  * @example
45
46
  * ```typescript
46
47
  * import env from '@/server/config/env.config';
47
48
  *
48
- * // Get required variable (throws if missing)
49
- * const dbUrl = env.require('DATABASE_URL');
50
- *
51
- * // Get optional variable (returns undefined if missing)
52
- * const redisUrl = env.get('REDIS_URL');
53
- *
54
- * // Get with default value
55
- * const port = env.get('PORT') ?? 8790;
49
+ * const dbUrl = env.DATABASE_URL; // throws if required and missing
50
+ * const port = env.PORT ?? 8790; // optional with fallback
56
51
  * ```
57
- */
58
- export const env = createEnvRegistry(envSchema);
59
-
60
- /**
61
- * Validate Environment Variables
62
52
  *
63
- * This validation runs automatically when this module is imported.
64
- * It will fail fast on startup if any required variables are missing
65
- * or if there are configuration errors.
53
+ * For explicit upfront validation (e.g. in a CLI script), call
54
+ * `createEnvRegistry(envSchema).validateAll()` to get `{ errors, warnings }`.
66
55
  */
67
- if (typeof process !== 'undefined' && process.env.NODE_ENV !== 'test')
68
- {
69
- const validation = env.validate();
70
-
71
- // Critical errors (missing required variables)
72
- if (!validation.valid)
73
- {
74
- console.error('\n❌ Environment Validation Failed\n');
75
- console.error('The following required environment variables are missing or invalid:\n');
76
-
77
- validation.errors.forEach((error) =>
78
- {
79
- console.error(` • ${error.key}`);
80
- console.error(` ${error.message}`);
81
-
82
- if (error.suggestion)
83
- {
84
- console.error(` 💡 ${error.suggestion}`);
85
- }
86
-
87
- console.error('');
88
- });
89
-
90
- console.error('Please check your .env files and ensure all required variables are set.');
91
- console.error('See .env.example for reference.\n');
92
-
93
- process.exit(1);
94
- }
56
+ export const env = createEnvRegistry(envSchema).validate();
95
57
 
96
- // Non-critical warnings
97
- if (validation.warnings.length > 0)
98
- {
99
- console.warn('\n⚠️ Environment Warnings:\n');
100
-
101
- validation.warnings.forEach((warning) =>
102
- {
103
- console.warn(` • ${warning.key}: ${warning.message}`);
104
-
105
- if (warning.suggestion)
106
- {
107
- console.warn(` 💡 ${warning.suggestion}`);
108
- }
109
- });
110
-
111
- console.warn('');
112
- }
113
- }
114
-
115
- /**
116
- * Export the registry as default
117
- *
118
- * Import this in your application to access environment variables:
119
- * ```typescript
120
- * import env from '@/server/config/env.config';
121
- * ```
122
- */
123
58
  export default env;
@@ -39,26 +39,16 @@ export const listExamples = route.get('/examples')
39
39
 
40
40
  /**
41
41
  * GET /examples/:id - Get single example
42
- *
43
- * This route requires authorization header for testing header validation
44
42
  */
45
43
  export const getExample = route.get('/examples/:id')
46
44
  .input({
47
- headers: Type.Object({
48
- test: Type.String({
49
- description: 'Bearer token for authentication'
50
- })
51
- }),
52
45
  params: Type.Object({
53
46
  id: Type.String(),
54
47
  }),
55
48
  })
56
49
  .handler(async (c) =>
57
50
  {
58
- const { params, headers } = await c.data();
59
-
60
- // Log authorization header for testing
61
- console.log('Authorization:', headers.test);
51
+ const { params } = await c.data();
62
52
 
63
53
  const example = await exampleRepo.findById(params.id);
64
54
 
@@ -16,7 +16,6 @@ export const getRoot = route.get('/')
16
16
  endpoints: {
17
17
  health: '/health',
18
18
  examples: '/examples',
19
- teams: '/teams',
20
19
  },
21
20
  message: 'Welcome to SPFN! Visit /examples for usage examples.',
22
21
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spfn",
3
- "version": "0.2.0-beta.46",
3
+ "version": "0.2.0-beta.48",
4
4
  "description": "Superfunction CLI - Add SPFN to your Next.js project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -78,7 +78,7 @@
78
78
  "concurrently": "^9.2.1",
79
79
  "tsx": "^4.20.6",
80
80
  "typescript": "^5.3.3",
81
- "@spfn/core": "0.2.0-beta.45"
81
+ "@spfn/core": "0.2.0-beta.49"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsup && node scripts/copy-templates.js",
@@ -1,58 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Contracts
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { Type } from '@sinclair/typebox';
8
- import { createContract } from '@spfn/core/route';
9
-
10
- /**
11
- * GET /{{ENTITY_NAME}}
12
- * List {{PASCAL_NAME}} items
13
- */
14
- export const list{{PASCAL_NAME}}Contract = createContract({
15
- method: 'GET',
16
- path: '/{{ENTITY_NAME}}',
17
- query: Type.Object({
18
- limit: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
19
- offset: Type.Optional(Type.Number({ minimum: 0 })),
20
- }),
21
- responses: {
22
- 200: Type.Object({
23
- items: Type.Array(
24
- Type.Object({
25
- id: Type.Number(),
26
- name: Type.String(),
27
- description: Type.Union([Type.String(), Type.Null()]),
28
- createdAt: Type.String(),
29
- updatedAt: Type.String(),
30
- })
31
- ),
32
- limit: Type.Number(),
33
- offset: Type.Number(),
34
- }),
35
- },
36
- });
37
-
38
- /**
39
- * POST /{{ENTITY_NAME}}
40
- * Create new {{PASCAL_NAME}}
41
- */
42
- export const create{{PASCAL_NAME}}Contract = createContract({
43
- method: 'POST',
44
- path: '/{{ENTITY_NAME}}',
45
- body: Type.Object({
46
- name: Type.String({ minLength: 1 }),
47
- description: Type.Optional(Type.String()),
48
- }),
49
- responses: {
50
- 201: Type.Object({
51
- id: Type.Number(),
52
- name: Type.String(),
53
- description: Type.Union([Type.String(), Type.Null()]),
54
- createdAt: Type.String(),
55
- updatedAt: Type.String(),
56
- }),
57
- },
58
- });
@@ -1,28 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Entity
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { index, serial, text, timestamp } from 'drizzle-orm/pg-core';
8
- import { {{SCHEMA_VAR_NAME}} } from './{{SCHEMA_FILE_NAME}}';
9
-
10
- export const {{ENTITY_NAME}} = {{SCHEMA_VAR_NAME}}.table('{{TABLE_NAME}}', {
11
- // Primary Key
12
- id: serial('id').primaryKey(),
13
-
14
- // Fields
15
- name: text('name').notNull(),
16
- description: text('description'),
17
-
18
- // Timestamps
19
- createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
20
- updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
21
- }, (table) => [
22
- // Indexes
23
- index('{{SCHEMA_NAME}}_{{TABLE_NAME}}_name_idx').on(table.name),
24
- ]);
25
-
26
- // Type inference
27
- export type {{PASCAL_NAME}} = typeof {{ENTITY_NAME}}.$inferSelect;
28
- export type New{{PASCAL_NAME}} = typeof {{ENTITY_NAME}}.$inferInsert;
@@ -1,3 +0,0 @@
1
- -- Initial migration for @spfn/{{FN_NAME}}
2
- -- Create schema for {{PASCAL_NAME}} package
3
- CREATE SCHEMA IF NOT EXISTS spfn_{{FN_NAME}};
@@ -1,56 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Repository
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { findOne, findMany, create, update, deleteById } from '@spfn/core/db';
8
- import { eq } from 'drizzle-orm';
9
- import { {{ENTITY_NAME}} } from '@/entities';
10
- import type { {{PASCAL_NAME}}, New{{PASCAL_NAME}} } from '@/entities';
11
-
12
- export const {{REPO_NAME}} = {
13
- /**
14
- * Find {{PASCAL_NAME}} by ID
15
- */
16
- findById: async (id: number): Promise<{{PASCAL_NAME}} | null> =>
17
- {
18
- return findOne({{ENTITY_NAME}}, eq({{ENTITY_NAME}}.id, id));
19
- },
20
-
21
- /**
22
- * Find all {{PASCAL_NAME}} items
23
- */
24
- findMany: async (options?: { limit?: number; offset?: number }): Promise<{{PASCAL_NAME}}[]> =>
25
- {
26
- const { limit = 20, offset = 0 } = options || {};
27
- return findMany({{ENTITY_NAME}}, {
28
- limit,
29
- offset,
30
- });
31
- },
32
-
33
- /**
34
- * Create new {{PASCAL_NAME}}
35
- */
36
- create: async (data: New{{PASCAL_NAME}}): Promise<{{PASCAL_NAME}}> =>
37
- {
38
- return create({{ENTITY_NAME}}, data);
39
- },
40
-
41
- /**
42
- * Update {{PASCAL_NAME}} by ID
43
- */
44
- updateById: async (id: number, data: Partial<New{{PASCAL_NAME}}>): Promise<{{PASCAL_NAME}} | null> =>
45
- {
46
- return update({{ENTITY_NAME}}, eq({{ENTITY_NAME}}.id, id), data);
47
- },
48
-
49
- /**
50
- * Delete {{PASCAL_NAME}} by ID
51
- */
52
- deleteById: async (id: number): Promise<void> =>
53
- {
54
- await deleteById({{ENTITY_NAME}}, id);
55
- },
56
- };
@@ -1,65 +0,0 @@
1
- /**
2
- * {{PASCAL_NAME}} Routes
3
- *
4
- * Auto-generated by SPFN CLI
5
- */
6
-
7
- import { createApp } from '@spfn/core/route';
8
- import { Transactional } from '@spfn/core/db';
9
- import { {{REPO_NAME}} } from '@/repositories/{{KEBAB_NAME}}.repository.js';
10
- import {
11
- list{{PASCAL_NAME}}Contract,
12
- create{{PASCAL_NAME}}Contract,
13
- } from '@/contracts/{{KEBAB_NAME}}.js';
14
-
15
- const app = createApp();
16
-
17
- /**
18
- * GET /{{ENTITY_NAME}}
19
- * List {{PASCAL_NAME}} items
20
- */
21
- app.bind(list{{PASCAL_NAME}}Contract, async (c) =>
22
- {
23
- const { limit = 20, offset = 0 } = c.query;
24
-
25
- const items = await {{REPO_NAME}}.findMany({ limit, offset });
26
-
27
- return c.json({
28
- items: items.map((item) => ({
29
- id: item.id,
30
- name: item.name,
31
- description: item.description,
32
- createdAt: item.createdAt.toISOString(),
33
- updatedAt: item.updatedAt.toISOString(),
34
- })),
35
- limit,
36
- offset,
37
- });
38
- });
39
-
40
- /**
41
- * POST /{{ENTITY_NAME}}
42
- * Create new {{PASCAL_NAME}}
43
- */
44
- app.bind(create{{PASCAL_NAME}}Contract, [Transactional()], async (c) =>
45
- {
46
- const body = await c.data();
47
-
48
- const item = await {{REPO_NAME}}.create({
49
- name: body.name,
50
- description: body.description,
51
- });
52
-
53
- return c.json(
54
- {
55
- id: item.id,
56
- name: item.name,
57
- description: item.description,
58
- createdAt: item.createdAt.toISOString(),
59
- updatedAt: item.updatedAt.toISOString(),
60
- },
61
- 201
62
- );
63
- });
64
-
65
- export default app;
@@ -1,13 +0,0 @@
1
- /**
2
- * {{FN_NAME_PASCAL}} Schema
3
- *
4
- * Creates isolated PostgreSQL schema for {{PACKAGE_NAME}} tables.
5
- * IMPORTANT: Export this schema so drizzle-kit can generate CREATE SCHEMA statement.
6
- *
7
- * Auto-generated by SPFN CLI
8
- */
9
-
10
- import { createFunctionSchema } from '@spfn/core/db';
11
-
12
- // ✅ Export schema to auto-generate CREATE SCHEMA in migrations
13
- export const {{SCHEMA_VAR_NAME}} = createFunctionSchema('{{PACKAGE_NAME}}');
@@ -1,67 +0,0 @@
1
- # Optimized Production Dockerfile for SPFN
2
- # Multi-stage build for smaller image size
3
-
4
- # Stage 1: Dependencies
5
- FROM node:20-alpine AS deps
6
- WORKDIR /app
7
-
8
- # Install pnpm
9
- RUN corepack enable pnpm
10
-
11
- # Copy dependency files
12
- COPY package.json pnpm-lock.yaml* ./
13
-
14
- # Install ALL dependencies (needed for build)
15
- RUN pnpm install --frozen-lockfile
16
-
17
- # Stage 2: Builder
18
- FROM node:20-alpine AS builder
19
- WORKDIR /app
20
-
21
- RUN corepack enable pnpm
22
-
23
- # Copy dependencies from deps stage
24
- COPY --from=deps /app/node_modules ./node_modules
25
- COPY . .
26
-
27
- # Build both Next.js and SPFN server
28
- RUN pnpm run spfn:build
29
-
30
- # Stage 3: Runner (Production)
31
- FROM node:20-alpine AS runner
32
- WORKDIR /app
33
-
34
- ENV NODE_ENV=production
35
-
36
- RUN corepack enable pnpm
37
-
38
- # Copy necessary files from builder
39
- COPY --from=builder /app/package.json ./package.json
40
- COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
41
- COPY --from=builder /app/.next ./.next
42
- COPY --from=builder /app/.spfn ./.spfn
43
- COPY --from=builder /app/public ./public
44
- COPY --from=builder /app/next.config.* ./
45
- COPY --from=builder /app/.env.local.example ./.env.local.example
46
-
47
- # Install PRODUCTION dependencies only
48
- RUN pnpm install --frozen-lockfile --prod
49
-
50
- # Create non-root user for security
51
- RUN addgroup --system --gid 1001 nodejs
52
- RUN adduser --system --uid 1001 nextjs
53
-
54
- # Change ownership
55
- RUN chown -R nextjs:nodejs /app
56
-
57
- USER nextjs
58
-
59
- # Expose both ports
60
- EXPOSE 3790 8790
61
-
62
- # Health check for SPFN API
63
- HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
64
- CMD node -e "require('http').get('http://localhost:8790/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
65
-
66
- # Start both Next.js and SPFN server
67
- CMD ["pnpm", "run", "spfn:start"]
@@ -1,22 +0,0 @@
1
- /**
2
- * Core Package Configuration
3
- *
4
- * @example
5
- * ```typescript
6
- * import { registry } from '@spfn/core/config';
7
- *
8
- * const env = registry.validate();
9
- * console.log(env.DB_POOL_MAX);
10
- * ```
11
- *
12
- * @module config
13
- */
14
-
15
- import { createEnvRegistry } from '@spfn/core/env';
16
- import { envSchema } from './schema';
17
-
18
- /**
19
- * Environment registry
20
- */
21
- const registry = createEnvRegistry(envSchema);
22
- export const env = registry.validate();
@@ -1,42 +0,0 @@
1
- /**
2
- * Environment Variable Schema
3
- *
4
- * Centralized schema definition for all environment variables.
5
- * This provides type safety, validation, and documentation for your application configuration.
6
- *
7
- * @example
8
- * ```typescript
9
- * import { envSchema } from './config/schema';
10
- *
11
- * // Access validated environment variables
12
- * console.log(envSchema.DATABASE_URL.description);
13
- * console.log(envSchema.PORT.default);
14
- * ```
15
- *
16
- * @module config/schema
17
- */
18
-
19
- import {
20
- defineEnvSchema,
21
- envString,
22
- envNumber,
23
- } from '@spfn/core/env';
24
-
25
- /**
26
- * Application environment variable schema
27
- *
28
- * Add your environment variables here with proper types and validation.
29
- */
30
- export const envSchema = defineEnvSchema({
31
- // Example: API configuration
32
- API_KEY: envString({
33
- description: 'Third-party API key',
34
- required: true,
35
- }),
36
-
37
- // Example: Server port
38
- PORT: envNumber({
39
- description: 'Server port number',
40
- default: 8790,
41
- }),
42
- });