@ankhorage/infra 0.0.0 → 0.1.0

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.
@@ -1,4 +1,4 @@
1
- import { generateDatasetInfrastructureArtifacts } from '../../datasets';
1
+ import { generateApiInfrastructureArtifacts } from '../../apis';
2
2
  import type {
3
3
  GeneratedPackageDependency,
4
4
  InfraManifestInput,
@@ -37,7 +37,7 @@ export function generateMinikubeInfra(
37
37
  appManifest: options.appManifest,
38
38
  });
39
39
  const storageArtifacts = generateStorageArtifacts({ manifest, namespace });
40
- const datasetArtifacts = generateDatasetInfrastructureArtifacts({
40
+ const apiArtifacts = generateApiInfrastructureArtifacts({
41
41
  data: options.appManifest?.data,
42
42
  databaseProvider: manifest.database?.provider,
43
43
  });
@@ -65,7 +65,7 @@ export function generateMinikubeInfra(
65
65
  ...authArtifacts.warnings,
66
66
  ...authzArtifacts.warnings,
67
67
  ...storageArtifacts.warnings,
68
- ...datasetArtifacts.warnings,
68
+ ...apiArtifacts.warnings,
69
69
  ]);
70
70
 
71
71
  return {
@@ -74,7 +74,7 @@ export function generateMinikubeInfra(
74
74
  ...authArtifacts.files,
75
75
  ...authzArtifacts.files,
76
76
  ...storageArtifacts.files,
77
- ...datasetArtifacts.files,
77
+ ...apiArtifacts.files,
78
78
  ],
79
79
  warnings,
80
80
  meta: {
@@ -0,0 +1,211 @@
1
+ import type { AppManifest } from '@ankhorage/contracts';
2
+ import { describe, expect, test } from 'bun:test';
3
+
4
+ import { generateInfrastructure } from './index';
5
+ import type { InfraManifestInput } from './types';
6
+
7
+ describe('API infrastructure generation', () => {
8
+ test('generates provider-neutral API, seed, handlers, and OpenAPI artifacts', () => {
9
+ const manifest: InfraManifestInput = {
10
+ deployment: {
11
+ target: 'minikube',
12
+ monitoring: false,
13
+ },
14
+ database: {
15
+ provider: 'supabase',
16
+ tier: 'dev',
17
+ },
18
+ plugins: [],
19
+ };
20
+
21
+ const appManifest: Pick<
22
+ AppManifest,
23
+ 'data' | 'metadata' | 'navigator' | 'screens' | 'settings'
24
+ > = {
25
+ metadata: {
26
+ name: 'Cards',
27
+ slug: 'cards',
28
+ version: '1.0.0',
29
+ themeId: 'default',
30
+ },
31
+ data: {
32
+ apis: {
33
+ poker_situations: {
34
+ id: 'poker_situations',
35
+ kind: 'generated',
36
+ label: 'Poker situations',
37
+ description: 'App-owned poker trainer situations.',
38
+ basePath: '/api/poker-situations',
39
+ preset: 'crud',
40
+ resource: {
41
+ kind: 'collection',
42
+ collection: {
43
+ name: 'poker_situations',
44
+ primaryKey: 'id',
45
+ fields: [
46
+ { name: 'id', type: 'uuid', required: true, unique: true },
47
+ { name: 'title', type: 'text', required: true },
48
+ { name: 'description', type: 'text', required: true },
49
+ { name: 'availableActions', type: 'json' },
50
+ { name: 'correctAction', type: 'text' },
51
+ ],
52
+ },
53
+ seed: [
54
+ {
55
+ title: 'Button faces a raise',
56
+ description: 'Choose the best action with position and stack depth in mind.',
57
+ availableActions: [
58
+ { label: 'Fold', value: 'fold' },
59
+ { label: 'Call', value: 'call' },
60
+ { label: 'Raise', value: 'raise' },
61
+ ],
62
+ correctAction: 'raise',
63
+ },
64
+ ],
65
+ },
66
+ endpoints: [
67
+ {
68
+ id: 'poker_situations.list',
69
+ method: 'GET',
70
+ path: '/',
71
+ intent: 'list',
72
+ },
73
+ {
74
+ id: 'poker_situations.read',
75
+ method: 'GET',
76
+ path: '/{id}',
77
+ intent: 'read',
78
+ },
79
+ {
80
+ id: 'poker_situations.create',
81
+ method: 'POST',
82
+ path: '/',
83
+ intent: 'create',
84
+ },
85
+ {
86
+ id: 'poker_situations.leaderboard',
87
+ method: 'GET',
88
+ path: '/leaderboard',
89
+ intent: 'custom',
90
+ },
91
+ ],
92
+ },
93
+ },
94
+ },
95
+ navigator: {
96
+ type: 'stack',
97
+ routes: [],
98
+ },
99
+ screens: {},
100
+ settings: {
101
+ localization: {
102
+ defaultLocale: 'en',
103
+ locales: ['en'],
104
+ },
105
+ authFlow: {
106
+ signInRoute: '/sign-in',
107
+ signUpRoute: '/sign-up',
108
+ signOutRoute: '/sign-out',
109
+ unauthorizedRoute: '/sign-in',
110
+ postSignInRoute: '/',
111
+ },
112
+ },
113
+ };
114
+
115
+ const result = generateInfrastructure(manifest, { namespaceHint: 'cards', appManifest });
116
+ const rerun = generateInfrastructure(manifest, { namespaceHint: 'cards', appManifest });
117
+ const paths = result.files.map((file) => file.path);
118
+
119
+ expect(result.files).toEqual(rerun.files);
120
+ expect(paths).toContain('infra/minikube/db/apis.json');
121
+ expect(paths).toContain('infra/minikube/db/api-seed.json');
122
+ expect(paths).toContain('infra/minikube/db/migrations/001_apis.sql');
123
+ expect(paths).toContain('infra/minikube/db/seeds/001_api_seed.sql');
124
+ expect(paths).toContain('infra/minikube/db/README.md');
125
+ expect(paths).toContain('src/generated/apis/resources.ts');
126
+ expect(paths).toContain('src/generated/apis/appApi.ts');
127
+ expect(paths).toContain('src/generated/apis/apiHandlers.ts');
128
+ expect(paths).toContain('src/generated/apis/openapi.json');
129
+ expect(paths.some((path) => path.includes('datasets'))).toBe(false);
130
+
131
+ const resourcesFile = result.files.find(
132
+ (file) => file.path === 'src/generated/apis/resources.ts',
133
+ );
134
+ expect(resourcesFile?.content).toContain('GENERATED_APIS');
135
+ expect(resourcesFile?.content).toContain('poker_situations');
136
+ expect(resourcesFile?.content).toContain('availableActions');
137
+
138
+ const apiFile = result.files.find((file) => file.path === 'src/generated/apis/appApi.ts');
139
+ expect(apiFile?.content).toContain('poker_situations.list');
140
+ expect(apiFile?.content).toContain('/api/poker-situations');
141
+ expect(apiFile?.content).toContain('poker_situations.read');
142
+ expect(apiFile?.content).toContain('/api/poker-situations/{id}');
143
+ expect(apiFile?.content).toContain('poker_situations.leaderboard');
144
+ expect(apiFile?.content).toContain('/api/poker-situations/leaderboard');
145
+
146
+ const handlersFile = result.files.find(
147
+ (file) => file.path === 'src/generated/apis/apiHandlers.ts',
148
+ );
149
+ expect(handlersFile?.content).toContain('handleGeneratedApiList');
150
+ expect(handlersFile?.content).toContain('handleGeneratedApiCreate');
151
+ expect(handlersFile?.content).toContain('createGeneratedApiMemoryStore');
152
+ expect(handlersFile?.content).toContain('poker_situations');
153
+ expect(handlersFile?.content).not.toContain('Dataset');
154
+
155
+ const openApiFile = result.files.find(
156
+ (file) => file.path === 'src/generated/apis/openapi.json',
157
+ );
158
+ expect(openApiFile?.content).toContain('Generated app API');
159
+ expect(openApiFile?.content).toContain('poker_situations.create');
160
+ expect(openApiFile?.content).toContain('poker_situations.leaderboard');
161
+ expect(openApiFile?.content).toContain('x-ankh-api');
162
+ expect(openApiFile?.content).not.toContain('x-ankh-dataset');
163
+
164
+ const migrationFile = result.files.find(
165
+ (file) => file.path === 'infra/minikube/db/migrations/001_apis.sql',
166
+ );
167
+ expect(migrationFile?.content).toContain('create extension if not exists pgcrypto');
168
+ expect(migrationFile?.content).toContain(
169
+ 'create table if not exists "public"."poker_situations"',
170
+ );
171
+ expect(migrationFile?.content).toContain('"id" uuid default gen_random_uuid() primary key');
172
+ expect(migrationFile?.content).toContain('"availableActions" jsonb');
173
+
174
+ const seedFile = result.files.find((file) => file.path === 'infra/minikube/db/api-seed.json');
175
+ expect(seedFile?.content).toContain('Button faces a raise');
176
+ expect(seedFile?.content).toContain('correctAction');
177
+ expect(seedFile?.content).toContain('"id"');
178
+
179
+ const seedSqlFile = result.files.find(
180
+ (file) => file.path === 'infra/minikube/db/seeds/001_api_seed.sql',
181
+ );
182
+ expect(seedSqlFile?.content).toContain('insert into "public"."poker_situations"');
183
+ expect(seedSqlFile?.content).toContain('Button faces a raise');
184
+ expect(seedSqlFile?.content).toContain('on conflict ("id") do nothing');
185
+ });
186
+
187
+ test('reports selected state adapter dependencies', () => {
188
+ const manifest: InfraManifestInput = {
189
+ deployment: {
190
+ target: 'minikube',
191
+ monitoring: false,
192
+ },
193
+ state: {
194
+ provider: 'legend',
195
+ persistence: 'none',
196
+ },
197
+ plugins: [],
198
+ };
199
+
200
+ const result = generateInfrastructure(manifest, { namespaceHint: 'cards' });
201
+
202
+ expect(result.meta.providers).toContain('legend');
203
+ expect(result.dependencies).toEqual([
204
+ {
205
+ name: '@ankhorage/state-legend',
206
+ version: '^0.1.0',
207
+ reason: 'Selected by infra.state.provider=legend.',
208
+ },
209
+ ]);
210
+ });
211
+ });