@objectifthunes/create-sandstone 1.0.0 → 1.1.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 (88) hide show
  1. package/dist/commands/add-adapter.js +36 -36
  2. package/dist/commands/add-adapter.js.map +1 -1
  3. package/dist/commands/generate-entity.d.ts.map +1 -1
  4. package/dist/commands/generate-entity.js +18 -11
  5. package/dist/commands/generate-entity.js.map +1 -1
  6. package/dist/generator.d.ts.map +1 -1
  7. package/dist/generator.js +89 -29
  8. package/dist/generator.js.map +1 -1
  9. package/dist/index.js +4 -4
  10. package/dist/index.js.map +1 -1
  11. package/dist/presets.d.ts +1 -1
  12. package/dist/presets.d.ts.map +1 -1
  13. package/dist/presets.js +8 -32
  14. package/dist/presets.js.map +1 -1
  15. package/dist/templates/auth-test.d.ts +3 -0
  16. package/dist/templates/auth-test.d.ts.map +1 -0
  17. package/dist/templates/auth-test.js +52 -0
  18. package/dist/templates/auth-test.js.map +1 -0
  19. package/dist/templates/claude-md.js +2 -2
  20. package/dist/templates/claude-md.js.map +1 -1
  21. package/dist/templates/entities-repositories.d.ts +3 -0
  22. package/dist/templates/entities-repositories.d.ts.map +1 -0
  23. package/dist/templates/entities-repositories.js +19 -0
  24. package/dist/templates/entities-repositories.js.map +1 -0
  25. package/dist/templates/entities-types.d.ts +3 -0
  26. package/dist/templates/entities-types.d.ts.map +1 -0
  27. package/dist/templates/entities-types.js +24 -0
  28. package/dist/templates/entities-types.js.map +1 -0
  29. package/dist/templates/graphql-auth.d.ts +3 -0
  30. package/dist/templates/graphql-auth.d.ts.map +1 -0
  31. package/dist/templates/graphql-auth.js +160 -0
  32. package/dist/templates/graphql-auth.js.map +1 -0
  33. package/dist/templates/graphql-enums.d.ts +2 -0
  34. package/dist/templates/graphql-enums.d.ts.map +1 -0
  35. package/dist/templates/graphql-enums.js +28 -0
  36. package/dist/templates/graphql-enums.js.map +1 -0
  37. package/dist/templates/graphql-helpers.d.ts +2 -0
  38. package/dist/templates/graphql-helpers.d.ts.map +1 -0
  39. package/dist/templates/graphql-helpers.js +142 -0
  40. package/dist/templates/graphql-helpers.js.map +1 -0
  41. package/dist/templates/graphql-inputs.d.ts +2 -0
  42. package/dist/templates/graphql-inputs.d.ts.map +1 -0
  43. package/dist/templates/graphql-inputs.js +28 -0
  44. package/dist/templates/graphql-inputs.js.map +1 -0
  45. package/dist/templates/graphql-schema.d.ts +3 -0
  46. package/dist/templates/graphql-schema.d.ts.map +1 -0
  47. package/dist/templates/graphql-schema.js +33 -0
  48. package/dist/templates/graphql-schema.js.map +1 -0
  49. package/dist/templates/graphql-types.d.ts +3 -0
  50. package/dist/templates/graphql-types.d.ts.map +1 -0
  51. package/dist/templates/graphql-types.js +61 -0
  52. package/dist/templates/graphql-types.js.map +1 -0
  53. package/dist/templates/graphql-validation.d.ts +3 -0
  54. package/dist/templates/graphql-validation.d.ts.map +1 -0
  55. package/dist/templates/graphql-validation.js +37 -0
  56. package/dist/templates/graphql-validation.js.map +1 -0
  57. package/dist/templates/health-e2e-test.d.ts +3 -0
  58. package/dist/templates/health-e2e-test.d.ts.map +1 -0
  59. package/dist/templates/health-e2e-test.js +50 -0
  60. package/dist/templates/health-e2e-test.js.map +1 -0
  61. package/dist/templates/infrastructure.d.ts.map +1 -1
  62. package/dist/templates/infrastructure.js +42 -14
  63. package/dist/templates/infrastructure.js.map +1 -1
  64. package/dist/templates/main-express.d.ts.map +1 -1
  65. package/dist/templates/main-express.js +56 -5
  66. package/dist/templates/main-express.js.map +1 -1
  67. package/dist/templates/main-fastify.d.ts.map +1 -1
  68. package/dist/templates/main-fastify.js +7 -1
  69. package/dist/templates/main-fastify.js.map +1 -1
  70. package/dist/templates/main-hono.d.ts.map +1 -1
  71. package/dist/templates/main-hono.js +7 -2
  72. package/dist/templates/main-hono.js.map +1 -1
  73. package/dist/templates/package-json.d.ts.map +1 -1
  74. package/dist/templates/package-json.js +3 -0
  75. package/dist/templates/package-json.js.map +1 -1
  76. package/dist/templates/vitest-config.d.ts +2 -0
  77. package/dist/templates/vitest-config.d.ts.map +1 -0
  78. package/dist/templates/vitest-config.js +13 -0
  79. package/dist/templates/vitest-config.js.map +1 -0
  80. package/dist/templates/vitest-e2e-config.d.ts +2 -0
  81. package/dist/templates/vitest-e2e-config.d.ts.map +1 -0
  82. package/dist/templates/vitest-e2e-config.js +16 -0
  83. package/dist/templates/vitest-e2e-config.js.map +1 -0
  84. package/dist/templates/workers-index.d.ts +3 -0
  85. package/dist/templates/workers-index.d.ts.map +1 -0
  86. package/dist/templates/workers-index.js +19 -0
  87. package/dist/templates/workers-index.js.map +1 -0
  88. package/package.json +4 -1
@@ -0,0 +1,142 @@
1
+ export function graphqlHelpersTemplate() {
2
+ return `import { UnauthorizedError, NotFoundError } from '@objectifthunes/sandstone-sdk';
3
+ import type { GraphQLContext } from '@objectifthunes/sandstone-sdk';
4
+ import { app as sandstoneApp } from '../infrastructure.js';
5
+
6
+ // ── 1. Auth guard (implemented) ──────────────────────────────────────────────
7
+ export function requireAuth(ctx: GraphQLContext) {
8
+ if (!ctx.user) throw new UnauthorizedError('Authentication required');
9
+ return ctx.user;
10
+ }
11
+
12
+ // ── 2. Ownership check pattern ───────────────────────────────────────────────
13
+ // Checks authz adapter first, then falls back to manual user_id comparison.
14
+ //
15
+ // export async function getOwnedEntity(ctx: GraphQLContext, userId: string, entityId: string) {
16
+ // const entity = await entityRepo.findById(ctx.db, entityId);
17
+ // if (!entity) throw new NotFoundError('Entity', entityId);
18
+ // if (sandstoneApp.authz) {
19
+ // const allowed = await sandstoneApp.authz.can(userId, 'read', 'entity', entityId);
20
+ // if (allowed) return entity;
21
+ // }
22
+ // if (entity.user_id !== userId) throw new NotFoundError('Entity', entityId);
23
+ // return entity;
24
+ // }
25
+
26
+ // ── 3. Cache read-through pattern ────────────────────────────────────────────
27
+ // Reads from cache first, falls back to DB, then populates cache with TTL.
28
+ // Always invalidate on mutation.
29
+ //
30
+ // export async function getCachedEntities(userId: string, page: number, perPage: number) {
31
+ // const cacheKey = \`entities:\${userId}:\${page}:\${perPage}\`;
32
+ // if (sandstoneApp.cache) {
33
+ // const cached = await sandstoneApp.cache.get(cacheKey);
34
+ // if (cached) return cached;
35
+ // }
36
+ // const result = await entityRepo.paginate(sandstoneApp.db, {
37
+ // where: { user_id: userId },
38
+ // page,
39
+ // perPage,
40
+ // });
41
+ // if (sandstoneApp.cache) {
42
+ // await sandstoneApp.cache.set(cacheKey, result, 60); // 60s TTL
43
+ // }
44
+ // return result;
45
+ // }
46
+ //
47
+ // export async function invalidateEntityCache(userId: string) {
48
+ // if (sandstoneApp.cache) {
49
+ // await sandstoneApp.cache.delete(\`entities:\${userId}:*\`);
50
+ // }
51
+ // }
52
+
53
+ // ── 4. Search indexing pattern ───────────────────────────────────────────────
54
+ // Index documents on create/update, remove on delete.
55
+ //
56
+ // export async function indexEntityInSearch(entity: { id: string; name: string; status: string; user_id: string }) {
57
+ // if (sandstoneApp.search) {
58
+ // await sandstoneApp.search.index('entity_idx', [{
59
+ // id: entity.id,
60
+ // document: {
61
+ // name: entity.name,
62
+ // status: entity.status,
63
+ // user_id: entity.user_id,
64
+ // },
65
+ // }]);
66
+ // }
67
+ // }
68
+ //
69
+ // export async function removeEntityFromSearch(entityId: string) {
70
+ // if (sandstoneApp.search) {
71
+ // await sandstoneApp.search.delete('entity_idx', [entityId]);
72
+ // }
73
+ // }
74
+
75
+ // ── 5. Queue enqueue pattern ─────────────────────────────────────────────────
76
+ // Offload heavy work to background workers.
77
+ //
78
+ // export async function enqueueEntityJob(entityId: string, action: string) {
79
+ // if (sandstoneApp.queue) {
80
+ // await sandstoneApp.queue.enqueue('entity-jobs', { entityId, action }, {
81
+ // maxAttempts: 3,
82
+ // backoff: 'exponential',
83
+ // backoffDelayMs: 1000,
84
+ // });
85
+ // }
86
+ // }
87
+
88
+ // ── 6. Audit logging pattern ─────────────────────────────────────────────────
89
+ // Log every mutation for compliance and debugging.
90
+ //
91
+ // export async function auditLog(actor: string, action: string, resource: string, resourceId: string) {
92
+ // if (sandstoneApp.audit) {
93
+ // await sandstoneApp.audit.log({
94
+ // actor,
95
+ // action,
96
+ // resource,
97
+ // resourceId,
98
+ // });
99
+ // }
100
+ // }
101
+
102
+ // ── 7. Tracing pattern ──────────────────────────────────────────────────────
103
+ // Wrap business operations in spans for observability.
104
+ //
105
+ // export async function withTrace<T>(name: string, fn: () => Promise<T>): Promise<T> {
106
+ // return sandstoneApp.tracer.withSpan(name, async (span) => {
107
+ // try {
108
+ // const result = await fn();
109
+ // span.setStatus('ok');
110
+ // return result;
111
+ // } catch (err) {
112
+ // span.setStatus('error', err instanceof Error ? err.message : 'unknown');
113
+ // throw err;
114
+ // }
115
+ // });
116
+ // }
117
+
118
+ // ── 8. Feature flag pattern ─────────────────────────────────────────────────
119
+ // Branch logic based on feature flags with user-level targeting.
120
+ //
121
+ // export async function isFeatureEnabled(flag: string, userId?: string): Promise<boolean> {
122
+ // if (!sandstoneApp.flags) return false;
123
+ // return sandstoneApp.flags.isEnabled(flag, userId ? { userId } : undefined);
124
+ // }
125
+ //
126
+ // Usage in a resolver:
127
+ // const useNewFlow = await isFeatureEnabled('new_checkout_flow', user.sub);
128
+ // if (useNewFlow) {
129
+ // return handleNewCheckoutFlow(args);
130
+ // }
131
+ // return handleLegacyCheckoutFlow(args);
132
+
133
+ // === Money Arithmetic (avoids float imprecision with NUMERIC columns) ===
134
+ // export function toCents(v: string | number): number { return Math.round(Number(v) * 100); }
135
+ // export function fromCents(c: number): string { return (c / 100).toFixed(2); }
136
+ // export function addMoney(a: string | number, b: string | number): string {
137
+ // return fromCents(toCents(a) + toCents(b));
138
+ // }
139
+ // export function isZero(v: string | number): boolean { return toCents(v) === 0; }
140
+ `;
141
+ }
142
+ //# sourceMappingURL=graphql-helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-helpers.js","sourceRoot":"","sources":["../../src/templates/graphql-helpers.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,sBAAsB;IACpC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0IR,CAAC;AACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function graphqlInputsTemplate(): string;
2
+ //# sourceMappingURL=graphql-inputs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-inputs.d.ts","sourceRoot":"","sources":["../../src/templates/graphql-inputs.ts"],"names":[],"mappings":"AAAA,wBAAgB,qBAAqB,IAAI,MAAM,CA0B9C"}
@@ -0,0 +1,28 @@
1
+ export function graphqlInputsTemplate() {
2
+ return `// Add your GraphQL input types here.
3
+ // Each input type maps to a mutation's args for structured input.
4
+ //
5
+ // Example:
6
+ // import { GraphQLInputObjectType, GraphQLString, GraphQLNonNull, GraphQLFloat } from '@objectifthunes/sandstone-sdk';
7
+ //
8
+ // export const CreateInvoiceInput = new GraphQLInputObjectType({
9
+ // name: 'CreateInvoiceInput',
10
+ // fields: () => ({
11
+ // clientId: { type: new GraphQLNonNull(GraphQLString) },
12
+ // description: { type: new GraphQLNonNull(GraphQLString) },
13
+ // amount: { type: new GraphQLNonNull(GraphQLFloat) },
14
+ // currency: { type: GraphQLString, defaultValue: 'EUR' },
15
+ // }),
16
+ // });
17
+ //
18
+ // export const UpdateInvoiceInput = new GraphQLInputObjectType({
19
+ // name: 'UpdateInvoiceInput',
20
+ // fields: () => ({
21
+ // description: { type: GraphQLString },
22
+ // amount: { type: GraphQLFloat },
23
+ // status: { type: GraphQLString },
24
+ // }),
25
+ // });
26
+ `;
27
+ }
28
+ //# sourceMappingURL=graphql-inputs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-inputs.js","sourceRoot":"","sources":["../../src/templates/graphql-inputs.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwBR,CAAC;AACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from '../presets.js';
2
+ export declare function graphqlSchemaTemplate(_config: ProjectConfig): string;
3
+ //# sourceMappingURL=graphql-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-schema.d.ts","sourceRoot":"","sources":["../../src/templates/graphql-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CA+BpE"}
@@ -0,0 +1,33 @@
1
+ export function graphqlSchemaTemplate(_config) {
2
+ return `import { buildSchema, GraphQLObjectType } from '@objectifthunes/sandstone-sdk';
3
+ import { authQueries, authMutations } from './auth.js';
4
+
5
+ // Import your domain modules here:
6
+ // import { clientQueries, clientMutations } from './clients.js';
7
+ // import { invoiceQueries, invoiceMutations } from './invoices.js';
8
+
9
+ const QueryType = new GraphQLObjectType({
10
+ name: 'Query',
11
+ fields: () => ({
12
+ ...authQueries,
13
+ // ...clientQueries,
14
+ // ...invoiceQueries,
15
+ }),
16
+ });
17
+
18
+ const MutationType = new GraphQLObjectType({
19
+ name: 'Mutation',
20
+ fields: () => ({
21
+ ...authMutations,
22
+ // ...clientMutations,
23
+ // ...invoiceMutations,
24
+ }),
25
+ });
26
+
27
+ export const schema = buildSchema({
28
+ query: QueryType,
29
+ mutation: MutationType,
30
+ });
31
+ `;
32
+ }
33
+ //# sourceMappingURL=graphql-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-schema.js","sourceRoot":"","sources":["../../src/templates/graphql-schema.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BR,CAAC;AACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from '../presets.js';
2
+ export declare function graphqlTypesTemplate(config: ProjectConfig): string;
3
+ //# sourceMappingURL=graphql-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-types.d.ts","sourceRoot":"","sources":["../../src/templates/graphql-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAkElE"}
@@ -0,0 +1,61 @@
1
+ export function graphqlTypesTemplate(config) {
2
+ const parts = [];
3
+ // ── Imports ──────────────────────────────────────────────────────────────
4
+ const graphqlImports = [
5
+ 'GraphQLObjectType',
6
+ 'GraphQLString',
7
+ 'GraphQLNonNull',
8
+ 'GraphQLBoolean',
9
+ 'GraphQLInt',
10
+ 'GraphQLList',
11
+ 'GraphQLID',
12
+ ];
13
+ parts.push(`import {
14
+ ${graphqlImports.join(',\n ')},
15
+ } from '@objectifthunes/sandstone-sdk';`);
16
+ parts.push('');
17
+ // ── AuthResultType ───────────────────────────────────────────────────────
18
+ parts.push(`export const AuthResultType = new GraphQLObjectType({
19
+ name: 'AuthResult',
20
+ fields: () => ({
21
+ accessToken: { type: new GraphQLNonNull(GraphQLString) },
22
+ refreshToken: { type: new GraphQLNonNull(GraphQLString) },
23
+ isNewUser: { type: new GraphQLNonNull(GraphQLBoolean) },
24
+ }),
25
+ });`);
26
+ parts.push('');
27
+ // ── MeType ───────────────────────────────────────────────────────────────
28
+ parts.push(`export const MeType = new GraphQLObjectType({
29
+ name: 'Me',
30
+ fields: () => ({
31
+ id: { type: new GraphQLNonNull(GraphQLID) },
32
+ email: { type: new GraphQLNonNull(GraphQLString) },
33
+ roles: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))) },
34
+ }),
35
+ });`);
36
+ parts.push('');
37
+ // ── PaginationInfoType ───────────────────────────────────────────────────
38
+ parts.push(`export const PaginationInfoType = new GraphQLObjectType({
39
+ name: 'PaginationInfo',
40
+ fields: () => ({
41
+ page: { type: new GraphQLNonNull(GraphQLInt) },
42
+ perPage: { type: new GraphQLNonNull(GraphQLInt) },
43
+ total: { type: new GraphQLNonNull(GraphQLInt) },
44
+ totalPages: { type: new GraphQLNonNull(GraphQLInt) },
45
+ }),
46
+ });`);
47
+ // ── UploadUrlResultType (conditional on storage) ─────────────────────────
48
+ if (config.storage) {
49
+ parts.push('');
50
+ parts.push(`export const UploadUrlResultType = new GraphQLObjectType({
51
+ name: 'UploadUrlResult',
52
+ fields: () => ({
53
+ url: { type: new GraphQLNonNull(GraphQLString) },
54
+ key: { type: new GraphQLNonNull(GraphQLString) },
55
+ }),
56
+ });`);
57
+ }
58
+ parts.push('');
59
+ return parts.join('\n');
60
+ }
61
+ //# sourceMappingURL=graphql-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-types.js","sourceRoot":"","sources":["../../src/templates/graphql-types.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,oBAAoB,CAAC,MAAqB;IACxD,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,4EAA4E;IAC5E,MAAM,cAAc,GAAG;QACrB,mBAAmB;QACnB,eAAe;QACf,gBAAgB;QAChB,gBAAgB;QAChB,YAAY;QACZ,aAAa;QACb,WAAW;KACZ,CAAC;IAEF,KAAK,CAAC,IAAI,CAAC;IACT,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC;wCACQ,CAAC,CAAC;IACxC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,4EAA4E;IAC5E,KAAK,CAAC,IAAI,CAAC;;;;;;;IAOT,CAAC,CAAC;IACJ,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,4EAA4E;IAC5E,KAAK,CAAC,IAAI,CAAC;;;;;;;IAOT,CAAC,CAAC;IACJ,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,4EAA4E;IAC5E,KAAK,CAAC,IAAI,CAAC;;;;;;;;IAQT,CAAC,CAAC;IAEJ,4EAA4E;IAC5E,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC;;;;;;IAMX,CAAC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from '../presets.js';
2
+ export declare function graphqlValidationTemplate(config: ProjectConfig): string;
3
+ //# sourceMappingURL=graphql-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-validation.d.ts","sourceRoot":"","sources":["../../src/templates/graphql-validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAwCvE"}
@@ -0,0 +1,37 @@
1
+ export function graphqlValidationTemplate(config) {
2
+ const hasPassword = config.auth === 'password+otp' || config.auth === 'all';
3
+ const parts = [];
4
+ parts.push(`import { z } from '@objectifthunes/sandstone-sdk';`);
5
+ parts.push('');
6
+ // ── RegisterSchema (conditional on password auth) ────────────────────────
7
+ if (hasPassword) {
8
+ parts.push(`export const RegisterSchema = z.object({
9
+ email: z.string().email('Invalid email address'),
10
+ password: z
11
+ .string()
12
+ .min(8, 'Password must be at least 8 characters')
13
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
14
+ .regex(/[0-9]/, 'Password must contain at least one number'),
15
+ });`);
16
+ parts.push('');
17
+ }
18
+ // ── Example validation schema (always included as commented reference) ──
19
+ parts.push(`// Add your validation schemas here. Use with validate(schema, data) in resolvers.
20
+ // Every mutation should validate input before any DB operation.
21
+ //
22
+ // Example:
23
+ // export const CreateEntitySchema = z.object({
24
+ // name: z.string().min(1, 'Name is required').max(200, 'Name too long'),
25
+ // email: z.string().email('Invalid email'),
26
+ // description: z.string().max(2000).optional(),
27
+ // quantity: z.number().int().min(1, 'Must be at least 1').max(10000),
28
+ // price: z.number().min(0, 'Price cannot be negative').max(999999.99),
29
+ // taxRate: z.number().min(0).max(100).optional(),
30
+ // status: z.enum(['draft', 'active', 'archived']).default('draft'),
31
+ // tags: z.array(z.string().max(50)).max(20).optional(),
32
+ // notes: z.string().max(5000).nullable().optional(),
33
+ // });`);
34
+ parts.push('');
35
+ return parts.join('\n');
36
+ }
37
+ //# sourceMappingURL=graphql-validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql-validation.js","sourceRoot":"","sources":["../../src/templates/graphql-validation.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,yBAAyB,CAAC,MAAqB;IAC7D,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,KAAK,cAAc,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC;IAE5E,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CAAC,oDAAoD,CAAC,CAAC;IACjE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,4EAA4E;IAC5E,IAAI,WAAW,EAAE,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC;;;;;;;IAOX,CAAC,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IAED,2EAA2E;IAC3E,KAAK,CAAC,IAAI,CAAC;;;;;;;;;;;;;;OAcN,CAAC,CAAC;IACP,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ProjectConfig } from '../presets.js';
2
+ export declare function healthE2eTestTemplate(_config: ProjectConfig): string;
3
+ //# sourceMappingURL=health-e2e-test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-e2e-test.d.ts","sourceRoot":"","sources":["../../src/templates/health-e2e-test.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAgDpE"}
@@ -0,0 +1,50 @@
1
+ export function healthE2eTestTemplate(_config) {
2
+ return `import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+ import type { Server } from 'node:http';
4
+
5
+ let baseUrl: string;
6
+ let server: Server;
7
+
8
+ describe('Health Check E2E', () => {
9
+ beforeAll(async () => {
10
+ const { app } = await import('../../src/infrastructure.js');
11
+ const { runMigrations } = await import('@objectifthunes/sandstone-sdk');
12
+
13
+ const migrationDir = new URL('../../migrations', import.meta.url).pathname;
14
+ await runMigrations(app.db, { includeBuiltIn: true, directory: migrationDir });
15
+
16
+ // Dynamic import of main module to start server
17
+ // Adjust this based on your framework
18
+ const express = await import('express');
19
+ const { toExpressHandler } = await import('@objectifthunes/sandstone-sdk/adapters');
20
+
21
+ const srv = express.default();
22
+ srv.use(express.default.json());
23
+ srv.get('/health', toExpressHandler(app.healthHandler));
24
+
25
+ await new Promise<void>((resolve) => {
26
+ server = srv.listen(0, () => {
27
+ const addr = server.address();
28
+ const port = typeof addr === 'object' && addr ? addr.port : 3099;
29
+ baseUrl = \`http://localhost:\${port}\`;
30
+ resolve();
31
+ });
32
+ });
33
+ }, 30_000);
34
+
35
+ afterAll(async () => {
36
+ server?.close();
37
+ const { app } = await import('../../src/infrastructure.js');
38
+ await app.shutdown();
39
+ }, 10_000);
40
+
41
+ it('health check returns healthy', async () => {
42
+ const res = await fetch(\`\${baseUrl}/health\`);
43
+ const body = await res.json();
44
+ expect(res.status).toBe(200);
45
+ expect(body.status).toBe('healthy');
46
+ });
47
+ });
48
+ `;
49
+ }
50
+ //# sourceMappingURL=health-e2e-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-e2e-test.js","sourceRoot":"","sources":["../../src/templates/health-e2e-test.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,qBAAqB,CAAC,OAAsB;IAC1D,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8CR,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"infrastructure.d.ts","sourceRoot":"","sources":["../../src/templates/infrastructure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CA2mBpE"}
1
+ {"version":3,"file":"infrastructure.d.ts","sourceRoot":"","sources":["../../src/templates/infrastructure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAioBpE"}
@@ -7,6 +7,32 @@ export function infrastructureTemplate(config) {
7
7
  const isSocketIo = config.realtime === 'socketio';
8
8
  // Core imports always needed
9
9
  imports.push(`import { createApp } from '@objectifthunes/sandstone-sdk'`);
10
+ // ── Env validation (production only) ──────────────────────────────────────
11
+ if (!devMode) {
12
+ const requiredVars = ['DATABASE_URL', 'JWT_SECRET'];
13
+ if (config.email === 'resend')
14
+ requiredVars.push('RESEND_API_KEY');
15
+ if (config.sms === 'twilio')
16
+ requiredVars.push('TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_FROM_NUMBER');
17
+ if (config.payments === 'stripe')
18
+ requiredVars.push('STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET');
19
+ if (config.payments === 'revenuecat')
20
+ requiredVars.push('REVENUECAT_API_KEY', 'REVENUECAT_WEBHOOK_SECRET');
21
+ if (config.payments === 'paddle')
22
+ requiredVars.push('PADDLE_API_KEY', 'PADDLE_WEBHOOK_SECRET');
23
+ if (config.payments === 'lemonsqueezy')
24
+ requiredVars.push('LEMONSQUEEZY_API_KEY', 'LEMONSQUEEZY_WEBHOOK_SECRET');
25
+ if (config.cache === 'upstash')
26
+ requiredVars.push('UPSTASH_REDIS_URL', 'UPSTASH_REDIS_TOKEN');
27
+ if (config.cache === 'redis')
28
+ requiredVars.push('REDIS_URL');
29
+ if (config.queue === 'bullmq')
30
+ requiredVars.push('REDIS_URL');
31
+ const envCheckLines = requiredVars.map((v) => `'${v}'`).join(', ');
32
+ inits.push(`const _requiredEnv = [${envCheckLines}]`);
33
+ inits.push(`const _missingEnv = _requiredEnv.filter((k) => !process.env[k])`);
34
+ inits.push(`if (_missingEnv.length > 0) throw new Error(\`Missing required env vars: \${_missingEnv.join(', ')}\`)`);
35
+ }
10
36
  // ── Database ──────────────────────────────────────────────────────────────
11
37
  if (devMode) {
12
38
  if (config.database === 'supabase') {
@@ -24,7 +50,12 @@ export function infrastructureTemplate(config) {
24
50
  }
25
51
  else {
26
52
  imports.push(`import { createPgClient } from '@objectifthunes/sandstone-sdk/pg'`);
27
- inits.push(`const db = createPgClient({ connectionString: process.env.DATABASE_URL! })`);
53
+ inits.push(`const db = createPgClient({
54
+ connectionString: process.env.DATABASE_URL!,
55
+ max: 20,
56
+ idleTimeoutMillis: 30_000,
57
+ connectionTimeoutMillis: 5_000,
58
+ })`);
28
59
  }
29
60
  appOptions.push('db');
30
61
  // ── Logger ────────────────────────────────────────────────────────────────
@@ -103,7 +134,6 @@ export function infrastructureTemplate(config) {
103
134
  envVars: `{
104
135
  clientId: process.env.GOOGLE_CLIENT_ID!,
105
136
  clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
106
- redirectUri: process.env.GOOGLE_REDIRECT_URI!,
107
137
  }`,
108
138
  },
109
139
  github: {
@@ -112,7 +142,6 @@ export function infrastructureTemplate(config) {
112
142
  envVars: `{
113
143
  clientId: process.env.GITHUB_CLIENT_ID!,
114
144
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
115
- redirectUri: process.env.GITHUB_REDIRECT_URI!,
116
145
  }`,
117
146
  },
118
147
  apple: {
@@ -120,7 +149,6 @@ export function infrastructureTemplate(config) {
120
149
  subpath: 'oauth-apple',
121
150
  envVars: `{
122
151
  clientId: process.env.APPLE_CLIENT_ID!,
123
- clientSecret: process.env.APPLE_CLIENT_SECRET!,
124
152
  teamId: process.env.APPLE_TEAM_ID!,
125
153
  keyId: process.env.APPLE_KEY_ID!,
126
154
  privateKey: process.env.APPLE_PRIVATE_KEY!,
@@ -132,7 +160,6 @@ export function infrastructureTemplate(config) {
132
160
  envVars: `{
133
161
  clientId: process.env.DISCORD_CLIENT_ID!,
134
162
  clientSecret: process.env.DISCORD_CLIENT_SECRET!,
135
- redirectUri: process.env.DISCORD_REDIRECT_URI!,
136
163
  }`,
137
164
  },
138
165
  };
@@ -162,7 +189,7 @@ export function infrastructureTemplate(config) {
162
189
  inits.push(`const sms = createTwilioSms({
163
190
  accountSid: process.env.TWILIO_ACCOUNT_SID!,
164
191
  authToken: process.env.TWILIO_AUTH_TOKEN!,
165
- from: process.env.TWILIO_FROM_NUMBER!,
192
+ defaultFrom: process.env.TWILIO_FROM_NUMBER!,
166
193
  })`);
167
194
  }
168
195
  appOptions.push('sms');
@@ -317,7 +344,10 @@ export function infrastructureTemplate(config) {
317
344
  else {
318
345
  imports.push(`import { createBullMQQueue } from '@objectifthunes/sandstone-sdk/bullmq'`);
319
346
  inits.push(`const queue = createBullMQQueue({
320
- connection: process.env.REDIS_URL!,
347
+ connection: {
348
+ host: new URL(process.env.REDIS_URL!).hostname,
349
+ port: Number(new URL(process.env.REDIS_URL!).port || 6379),
350
+ },
321
351
  })`);
322
352
  }
323
353
  appOptions.push('queue');
@@ -397,7 +427,7 @@ export function infrastructureTemplate(config) {
397
427
  imports.push(`import { createPgAuthz } from '@objectifthunes/sandstone-sdk/pg-authz'`);
398
428
  inits.push(`const authz = createPgAuthz({ db })`);
399
429
  }
400
- appOptions.push('authz');
430
+ appOptions.push('authorization: authz');
401
431
  }
402
432
  else if (config.authz === 'casbin') {
403
433
  if (devMode) {
@@ -411,7 +441,7 @@ export function infrastructureTemplate(config) {
411
441
  policyPath: './casbin/policy.csv',
412
442
  })`);
413
443
  }
414
- appOptions.push('authz');
444
+ appOptions.push('authorization: authz');
415
445
  }
416
446
  // ── Payments ──────────────────────────────────────────────────────────────
417
447
  if (config.payments === 'stripe') {
@@ -482,10 +512,8 @@ export function infrastructureTemplate(config) {
482
512
  inits.push(`const storage = createS3Storage({
483
513
  bucket: process.env.S3_BUCKET!,
484
514
  region: process.env.S3_REGION!,
485
- credentials: {
486
- accessKeyId: process.env.S3_ACCESS_KEY_ID!,
487
- secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
488
- },
515
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
516
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
489
517
  })`);
490
518
  }
491
519
  }
@@ -554,7 +582,7 @@ export function infrastructureTemplate(config) {
554
582
  }
555
583
  // ── GraphQL ───────────────────────────────────────────────────────────────
556
584
  if (config.graphql) {
557
- imports.push(`import { schema } from './schema.js'`);
585
+ imports.push(`import { schema } from './graphql/schema.js'`);
558
586
  appOptions.push(`graphql: { schema }`);
559
587
  }
560
588
  // ── Dashboard ─────────────────────────────────────────────────────────────