@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.
- package/dist/commands/add-adapter.js +36 -36
- package/dist/commands/add-adapter.js.map +1 -1
- package/dist/commands/generate-entity.d.ts.map +1 -1
- package/dist/commands/generate-entity.js +18 -11
- package/dist/commands/generate-entity.js.map +1 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +89 -29
- package/dist/generator.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js +8 -32
- package/dist/presets.js.map +1 -1
- package/dist/templates/auth-test.d.ts +3 -0
- package/dist/templates/auth-test.d.ts.map +1 -0
- package/dist/templates/auth-test.js +52 -0
- package/dist/templates/auth-test.js.map +1 -0
- package/dist/templates/claude-md.js +2 -2
- package/dist/templates/claude-md.js.map +1 -1
- package/dist/templates/entities-repositories.d.ts +3 -0
- package/dist/templates/entities-repositories.d.ts.map +1 -0
- package/dist/templates/entities-repositories.js +19 -0
- package/dist/templates/entities-repositories.js.map +1 -0
- package/dist/templates/entities-types.d.ts +3 -0
- package/dist/templates/entities-types.d.ts.map +1 -0
- package/dist/templates/entities-types.js +24 -0
- package/dist/templates/entities-types.js.map +1 -0
- package/dist/templates/graphql-auth.d.ts +3 -0
- package/dist/templates/graphql-auth.d.ts.map +1 -0
- package/dist/templates/graphql-auth.js +160 -0
- package/dist/templates/graphql-auth.js.map +1 -0
- package/dist/templates/graphql-enums.d.ts +2 -0
- package/dist/templates/graphql-enums.d.ts.map +1 -0
- package/dist/templates/graphql-enums.js +28 -0
- package/dist/templates/graphql-enums.js.map +1 -0
- package/dist/templates/graphql-helpers.d.ts +2 -0
- package/dist/templates/graphql-helpers.d.ts.map +1 -0
- package/dist/templates/graphql-helpers.js +142 -0
- package/dist/templates/graphql-helpers.js.map +1 -0
- package/dist/templates/graphql-inputs.d.ts +2 -0
- package/dist/templates/graphql-inputs.d.ts.map +1 -0
- package/dist/templates/graphql-inputs.js +28 -0
- package/dist/templates/graphql-inputs.js.map +1 -0
- package/dist/templates/graphql-schema.d.ts +3 -0
- package/dist/templates/graphql-schema.d.ts.map +1 -0
- package/dist/templates/graphql-schema.js +33 -0
- package/dist/templates/graphql-schema.js.map +1 -0
- package/dist/templates/graphql-types.d.ts +3 -0
- package/dist/templates/graphql-types.d.ts.map +1 -0
- package/dist/templates/graphql-types.js +61 -0
- package/dist/templates/graphql-types.js.map +1 -0
- package/dist/templates/graphql-validation.d.ts +3 -0
- package/dist/templates/graphql-validation.d.ts.map +1 -0
- package/dist/templates/graphql-validation.js +37 -0
- package/dist/templates/graphql-validation.js.map +1 -0
- package/dist/templates/health-e2e-test.d.ts +3 -0
- package/dist/templates/health-e2e-test.d.ts.map +1 -0
- package/dist/templates/health-e2e-test.js +50 -0
- package/dist/templates/health-e2e-test.js.map +1 -0
- package/dist/templates/infrastructure.d.ts.map +1 -1
- package/dist/templates/infrastructure.js +42 -14
- package/dist/templates/infrastructure.js.map +1 -1
- package/dist/templates/main-express.d.ts.map +1 -1
- package/dist/templates/main-express.js +56 -5
- package/dist/templates/main-express.js.map +1 -1
- package/dist/templates/main-fastify.d.ts.map +1 -1
- package/dist/templates/main-fastify.js +7 -1
- package/dist/templates/main-fastify.js.map +1 -1
- package/dist/templates/main-hono.d.ts.map +1 -1
- package/dist/templates/main-hono.js +7 -2
- package/dist/templates/main-hono.js.map +1 -1
- package/dist/templates/package-json.d.ts.map +1 -1
- package/dist/templates/package-json.js +3 -0
- package/dist/templates/package-json.js.map +1 -1
- package/dist/templates/vitest-config.d.ts +2 -0
- package/dist/templates/vitest-config.d.ts.map +1 -0
- package/dist/templates/vitest-config.js +13 -0
- package/dist/templates/vitest-config.js.map +1 -0
- package/dist/templates/vitest-e2e-config.d.ts +2 -0
- package/dist/templates/vitest-e2e-config.d.ts.map +1 -0
- package/dist/templates/vitest-e2e-config.js +16 -0
- package/dist/templates/vitest-e2e-config.js.map +1 -0
- package/dist/templates/workers-index.d.ts +3 -0
- package/dist/templates/workers-index.d.ts.map +1 -0
- package/dist/templates/workers-index.js +19 -0
- package/dist/templates/workers-index.js.map +1 -0
- 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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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,
|
|
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({
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
486
|
-
|
|
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 ─────────────────────────────────────────────────────────────
|