@geekmidas/cli 0.53.0 → 1.0.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.
- package/CHANGELOG.md +17 -0
- package/README.md +26 -5
- package/dist/CachedStateProvider-D73dCqfH.cjs +60 -0
- package/dist/CachedStateProvider-D73dCqfH.cjs.map +1 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs +54 -0
- package/dist/CachedStateProvider-DVyKfaMm.mjs.map +1 -0
- package/dist/CachedStateProvider-D_uISMmJ.cjs +3 -0
- package/dist/CachedStateProvider-OiFUGr7p.mjs +3 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs +210 -0
- package/dist/HostingerProvider-DUV9-Tzg.cjs.map +1 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs +210 -0
- package/dist/HostingerProvider-DqUq6e9i.mjs.map +1 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs +43 -0
- package/dist/LocalStateProvider-CdspeSVL.cjs.map +1 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs +42 -0
- package/dist/LocalStateProvider-DxoSaWUV.mjs.map +1 -0
- package/dist/Route53Provider-CpRIqu69.cjs +157 -0
- package/dist/Route53Provider-CpRIqu69.cjs.map +1 -0
- package/dist/Route53Provider-KUAX3vz9.mjs +156 -0
- package/dist/Route53Provider-KUAX3vz9.mjs.map +1 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs +53 -0
- package/dist/SSMStateProvider-BxAPU99a.cjs.map +1 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs +52 -0
- package/dist/SSMStateProvider-C4wp4AZe.mjs.map +1 -0
- package/dist/{bundler-DGry2vaR.mjs → bundler-BqTN5Dj5.mjs} +3 -3
- package/dist/{bundler-DGry2vaR.mjs.map → bundler-BqTN5Dj5.mjs.map} +1 -1
- package/dist/{bundler-BB-kETMd.cjs → bundler-tHLLwYuU.cjs} +3 -3
- package/dist/{bundler-BB-kETMd.cjs.map → bundler-tHLLwYuU.cjs.map} +1 -1
- package/dist/{config-HYiM3iQJ.cjs → config-BGeJsW1r.cjs} +2 -2
- package/dist/{config-HYiM3iQJ.cjs.map → config-BGeJsW1r.cjs.map} +1 -1
- package/dist/{config-C3LSBNSl.mjs → config-C6awcFBx.mjs} +2 -2
- package/dist/{config-C3LSBNSl.mjs.map → config-C6awcFBx.mjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/credentials-C8DWtnMY.cjs +174 -0
- package/dist/credentials-C8DWtnMY.cjs.map +1 -0
- package/dist/credentials-DT1dSxIx.mjs +126 -0
- package/dist/credentials-DT1dSxIx.mjs.map +1 -0
- package/dist/deploy/sniffer-envkit-patch.cjs.map +1 -1
- package/dist/deploy/sniffer-envkit-patch.mjs.map +1 -1
- package/dist/deploy/sniffer-loader.cjs +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs → dokploy-api-7k3t7_zd.mjs} +1 -1
- package/dist/{dokploy-api-94KzmTVf.mjs.map → dokploy-api-7k3t7_zd.mjs.map} +1 -1
- package/dist/dokploy-api-CHa8G51l.mjs +3 -0
- package/dist/{dokploy-api-YD8WCQfW.cjs → dokploy-api-CQvhV6Hd.cjs} +1 -1
- package/dist/{dokploy-api-YD8WCQfW.cjs.map → dokploy-api-CQvhV6Hd.cjs.map} +1 -1
- package/dist/dokploy-api-CWc02yyg.cjs +3 -0
- package/dist/{encryption-DaCB_NmS.cjs → encryption-BE0UOb8j.cjs} +1 -1
- package/dist/{encryption-DaCB_NmS.cjs.map → encryption-BE0UOb8j.cjs.map} +1 -1
- package/dist/{encryption-Biq0EZ4m.cjs → encryption-Cv3zips0.cjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs → encryption-JtMsiGNp.mjs} +1 -1
- package/dist/{encryption-BC4MAODn.mjs.map → encryption-JtMsiGNp.mjs.map} +1 -1
- package/dist/encryption-UUmaWAmz.mjs +3 -0
- package/dist/{index-pOA56MWT.d.cts → index-B5rGIc4g.d.cts} +553 -196
- package/dist/index-B5rGIc4g.d.cts.map +1 -0
- package/dist/{index-A70abJ1m.d.mts → index-KFEbMIRa.d.mts} +554 -197
- package/dist/index-KFEbMIRa.d.mts.map +1 -0
- package/dist/index.cjs +2242 -568
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2219 -545
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-C3C-BzIZ.mjs → openapi-BMFmLnX6.mjs} +51 -7
- package/dist/openapi-BMFmLnX6.mjs.map +1 -0
- package/dist/{openapi-D7WwlpPF.cjs → openapi-D1KXv2Ml.cjs} +51 -7
- package/dist/openapi-D1KXv2Ml.cjs.map +1 -0
- package/dist/{openapi-react-query-C_MxpBgF.cjs → openapi-react-query-BeXvk-wa.cjs} +1 -1
- package/dist/{openapi-react-query-C_MxpBgF.cjs.map → openapi-react-query-BeXvk-wa.cjs.map} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs → openapi-react-query-DGEkD39r.mjs} +1 -1
- package/dist/{openapi-react-query-ZoP9DPbY.mjs.map → openapi-react-query-DGEkD39r.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.cts +1 -1
- package/dist/openapi.d.mts +2 -2
- package/dist/openapi.mjs +3 -3
- package/dist/{storage-Dhst7BhI.mjs → storage-BMW6yLu3.mjs} +1 -1
- package/dist/{storage-Dhst7BhI.mjs.map → storage-BMW6yLu3.mjs.map} +1 -1
- package/dist/{storage-fOR8dMu5.cjs → storage-C7pmBq1u.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs → storage-CoCNe0Pt.cjs} +1 -1
- package/dist/{storage-BPRgh3DU.cjs.map → storage-CoCNe0Pt.cjs.map} +1 -1
- package/dist/{storage-DNj_I11J.mjs → storage-D8XzjVaO.mjs} +1 -1
- package/dist/{types-BtGL-8QS.d.mts → types-BldpmqQX.d.mts} +1 -1
- package/dist/{types-BtGL-8QS.d.mts.map → types-BldpmqQX.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +1 -1
- package/dist/workspace/index.d.mts +2 -2
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-CaVW6j2q.cjs → workspace-BFRUOOrh.cjs} +309 -25
- package/dist/workspace-BFRUOOrh.cjs.map +1 -0
- package/dist/{workspace-DLFRaDc-.mjs → workspace-DAxG3_H2.mjs} +309 -25
- package/dist/workspace-DAxG3_H2.mjs.map +1 -0
- package/package.json +12 -8
- package/src/build/__tests__/handler-templates.spec.ts +115 -47
- package/src/deploy/CachedStateProvider.ts +86 -0
- package/src/deploy/LocalStateProvider.ts +57 -0
- package/src/deploy/SSMStateProvider.ts +93 -0
- package/src/deploy/StateProvider.ts +171 -0
- package/src/deploy/__tests__/CachedStateProvider.spec.ts +228 -0
- package/src/deploy/__tests__/HostingerProvider.spec.ts +347 -0
- package/src/deploy/__tests__/LocalStateProvider.spec.ts +126 -0
- package/src/deploy/__tests__/Route53Provider.spec.ts +402 -0
- package/src/deploy/__tests__/SSMStateProvider.spec.ts +177 -0
- package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +1 -3
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/auth.ts +16 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/health.ts +13 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/endpoints/users.ts +15 -0
- package/src/deploy/__tests__/__fixtures__/route-apps/services.ts +55 -0
- package/src/deploy/__tests__/createDnsProvider.spec.ts +172 -0
- package/src/deploy/__tests__/createStateProvider.spec.ts +116 -0
- package/src/deploy/__tests__/dns-orchestration.spec.ts +192 -0
- package/src/deploy/__tests__/dns-verification.spec.ts +2 -2
- package/src/deploy/__tests__/env-resolver.spec.ts +41 -17
- package/src/deploy/__tests__/sniffer.spec.ts +168 -10
- package/src/deploy/__tests__/state.spec.ts +13 -5
- package/src/deploy/dns/DnsProvider.ts +163 -0
- package/src/deploy/dns/HostingerProvider.ts +100 -0
- package/src/deploy/dns/Route53Provider.ts +256 -0
- package/src/deploy/dns/index.ts +257 -165
- package/src/deploy/env-resolver.ts +12 -5
- package/src/deploy/index.ts +16 -13
- package/src/deploy/sniffer-envkit-patch.ts +3 -1
- package/src/deploy/sniffer-routes-worker.ts +104 -0
- package/src/deploy/sniffer.ts +130 -5
- package/src/deploy/state-commands.ts +274 -0
- package/src/dev/__tests__/entry.spec.ts +8 -2
- package/src/dev/__tests__/index.spec.ts +1 -3
- package/src/dev/index.ts +9 -3
- package/src/docker/__tests__/templates.spec.ts +3 -1
- package/src/docker/templates.ts +3 -3
- package/src/index.ts +88 -0
- package/src/init/__tests__/generators.spec.ts +273 -0
- package/src/init/__tests__/init.spec.ts +3 -3
- package/src/init/generators/auth.ts +1 -0
- package/src/init/generators/config.ts +2 -0
- package/src/init/generators/models.ts +6 -1
- package/src/init/generators/monorepo.ts +3 -0
- package/src/init/generators/ui.ts +1472 -0
- package/src/init/generators/web.ts +134 -87
- package/src/init/index.ts +22 -3
- package/src/init/templates/api.ts +109 -3
- package/src/openapi.ts +99 -13
- package/src/workspace/__tests__/schema.spec.ts +107 -0
- package/src/workspace/schema.ts +314 -4
- package/src/workspace/types.ts +22 -36
- package/dist/dokploy-api-CItuaWTq.mjs +0 -3
- package/dist/dokploy-api-DBNE8MDt.cjs +0 -3
- package/dist/encryption-CQXBZGkt.mjs +0 -3
- package/dist/index-A70abJ1m.d.mts.map +0 -1
- package/dist/index-pOA56MWT.d.cts.map +0 -1
- package/dist/openapi-C3C-BzIZ.mjs.map +0 -1
- package/dist/openapi-D7WwlpPF.cjs.map +0 -1
- package/dist/workspace-CaVW6j2q.cjs.map +0 -1
- package/dist/workspace-DLFRaDc-.mjs.map +0 -1
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeResourceRecordSetsCommand,
|
|
3
|
+
CreateHostedZoneCommand,
|
|
4
|
+
DeleteHostedZoneCommand,
|
|
5
|
+
ListResourceRecordSetsCommand,
|
|
6
|
+
Route53Client,
|
|
7
|
+
} from '@aws-sdk/client-route-53';
|
|
8
|
+
import {
|
|
9
|
+
afterAll,
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeAll,
|
|
12
|
+
beforeEach,
|
|
13
|
+
describe,
|
|
14
|
+
expect,
|
|
15
|
+
it,
|
|
16
|
+
} from 'vitest';
|
|
17
|
+
import { Route53Provider } from '../dns/Route53Provider';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Route53Provider Tests
|
|
21
|
+
*
|
|
22
|
+
* These tests require LocalStack to be running with Route53 enabled.
|
|
23
|
+
* Run: docker compose up -d localstack
|
|
24
|
+
*/
|
|
25
|
+
describe('Route53Provider', () => {
|
|
26
|
+
const LOCALSTACK_ENDPOINT = 'http://localhost:4566';
|
|
27
|
+
const TEST_DOMAIN = 'test-example.com';
|
|
28
|
+
let client: Route53Client;
|
|
29
|
+
let provider: Route53Provider;
|
|
30
|
+
let hostedZoneId: string;
|
|
31
|
+
|
|
32
|
+
beforeAll(async () => {
|
|
33
|
+
process.env.AWS_ACCESS_KEY_ID = 'test';
|
|
34
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'test';
|
|
35
|
+
process.env.AWS_REGION = 'us-east-1';
|
|
36
|
+
|
|
37
|
+
client = new Route53Client({
|
|
38
|
+
region: 'us-east-1',
|
|
39
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
40
|
+
credentials: {
|
|
41
|
+
accessKeyId: 'test',
|
|
42
|
+
secretAccessKey: 'test',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Create a hosted zone for testing
|
|
47
|
+
const createResponse = await client.send(
|
|
48
|
+
new CreateHostedZoneCommand({
|
|
49
|
+
Name: TEST_DOMAIN,
|
|
50
|
+
CallerReference: `test-${Date.now()}`,
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
hostedZoneId = createResponse.HostedZone!.Id!.replace('/hostedzone/', '');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
provider = new Route53Provider({
|
|
59
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
60
|
+
hostedZoneId,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
// Clean up any test records (excluding NS and SOA which are auto-created)
|
|
66
|
+
try {
|
|
67
|
+
const response = await client.send(
|
|
68
|
+
new ListResourceRecordSetsCommand({
|
|
69
|
+
HostedZoneId: hostedZoneId,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const recordsToDelete = (response.ResourceRecordSets ?? []).filter(
|
|
74
|
+
(record) => record.Type !== 'NS' && record.Type !== 'SOA',
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (recordsToDelete.length > 0) {
|
|
78
|
+
await client.send(
|
|
79
|
+
new ChangeResourceRecordSetsCommand({
|
|
80
|
+
HostedZoneId: hostedZoneId,
|
|
81
|
+
ChangeBatch: {
|
|
82
|
+
Changes: recordsToDelete.map((record) => ({
|
|
83
|
+
Action: 'DELETE',
|
|
84
|
+
ResourceRecordSet: record,
|
|
85
|
+
})),
|
|
86
|
+
},
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Ignore errors during cleanup
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
afterAll(async () => {
|
|
96
|
+
// Delete the hosted zone - need to remove all non-default records first
|
|
97
|
+
try {
|
|
98
|
+
const response = await client.send(
|
|
99
|
+
new ListResourceRecordSetsCommand({
|
|
100
|
+
HostedZoneId: hostedZoneId,
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const recordsToDelete = (response.ResourceRecordSets ?? []).filter(
|
|
105
|
+
(record) => record.Type !== 'NS' && record.Type !== 'SOA',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if (recordsToDelete.length > 0) {
|
|
109
|
+
await client.send(
|
|
110
|
+
new ChangeResourceRecordSetsCommand({
|
|
111
|
+
HostedZoneId: hostedZoneId,
|
|
112
|
+
ChangeBatch: {
|
|
113
|
+
Changes: recordsToDelete.map((record) => ({
|
|
114
|
+
Action: 'DELETE',
|
|
115
|
+
ResourceRecordSet: record,
|
|
116
|
+
})),
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await client.send(
|
|
123
|
+
new DeleteHostedZoneCommand({
|
|
124
|
+
Id: hostedZoneId,
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
// Ignore cleanup errors
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
client.destroy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('name', () => {
|
|
135
|
+
it('should have name "route53"', () => {
|
|
136
|
+
expect(provider.name).toBe('route53');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('getRecords', () => {
|
|
141
|
+
it('should return empty array for domain with no custom records', async () => {
|
|
142
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
143
|
+
|
|
144
|
+
// Should be empty - NS and SOA are filtered out
|
|
145
|
+
expect(records).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should return A records', async () => {
|
|
149
|
+
// Create a test record (use UPSERT to handle idempotency)
|
|
150
|
+
await client.send(
|
|
151
|
+
new ChangeResourceRecordSetsCommand({
|
|
152
|
+
HostedZoneId: hostedZoneId,
|
|
153
|
+
ChangeBatch: {
|
|
154
|
+
Changes: [
|
|
155
|
+
{
|
|
156
|
+
Action: 'UPSERT',
|
|
157
|
+
ResourceRecordSet: {
|
|
158
|
+
Name: `api.${TEST_DOMAIN}`,
|
|
159
|
+
Type: 'A',
|
|
160
|
+
TTL: 300,
|
|
161
|
+
ResourceRecords: [{ Value: '1.2.3.4' }],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
170
|
+
|
|
171
|
+
expect(records).toHaveLength(1);
|
|
172
|
+
expect(records[0]).toEqual({
|
|
173
|
+
name: 'api',
|
|
174
|
+
type: 'A',
|
|
175
|
+
ttl: 300,
|
|
176
|
+
values: ['1.2.3.4'],
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should handle root domain records (@)', async () => {
|
|
181
|
+
// Create a root domain record (use UPSERT to handle idempotency)
|
|
182
|
+
await client.send(
|
|
183
|
+
new ChangeResourceRecordSetsCommand({
|
|
184
|
+
HostedZoneId: hostedZoneId,
|
|
185
|
+
ChangeBatch: {
|
|
186
|
+
Changes: [
|
|
187
|
+
{
|
|
188
|
+
Action: 'UPSERT',
|
|
189
|
+
ResourceRecordSet: {
|
|
190
|
+
Name: TEST_DOMAIN,
|
|
191
|
+
Type: 'A',
|
|
192
|
+
TTL: 300,
|
|
193
|
+
ResourceRecords: [{ Value: '1.2.3.4' }],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
202
|
+
|
|
203
|
+
expect(records).toHaveLength(1);
|
|
204
|
+
expect(records[0]?.name).toBe('@');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should return multiple records', async () => {
|
|
208
|
+
// Create multiple test records (use UPSERT to handle idempotency)
|
|
209
|
+
await client.send(
|
|
210
|
+
new ChangeResourceRecordSetsCommand({
|
|
211
|
+
HostedZoneId: hostedZoneId,
|
|
212
|
+
ChangeBatch: {
|
|
213
|
+
Changes: [
|
|
214
|
+
{
|
|
215
|
+
Action: 'UPSERT',
|
|
216
|
+
ResourceRecordSet: {
|
|
217
|
+
Name: `api.${TEST_DOMAIN}`,
|
|
218
|
+
Type: 'A',
|
|
219
|
+
TTL: 300,
|
|
220
|
+
ResourceRecords: [{ Value: '1.2.3.4' }],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
Action: 'UPSERT',
|
|
225
|
+
ResourceRecordSet: {
|
|
226
|
+
Name: `www.${TEST_DOMAIN}`,
|
|
227
|
+
Type: 'CNAME',
|
|
228
|
+
TTL: 300,
|
|
229
|
+
ResourceRecords: [{ Value: TEST_DOMAIN }],
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
238
|
+
|
|
239
|
+
expect(records).toHaveLength(2);
|
|
240
|
+
expect(records.find((r) => r.name === 'api')).toBeDefined();
|
|
241
|
+
expect(records.find((r) => r.name === 'www')).toBeDefined();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('upsertRecords', () => {
|
|
246
|
+
it('should create new records', async () => {
|
|
247
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
248
|
+
{ name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
249
|
+
]);
|
|
250
|
+
|
|
251
|
+
expect(results).toHaveLength(1);
|
|
252
|
+
expect(results[0]).toEqual({
|
|
253
|
+
record: { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
254
|
+
created: true,
|
|
255
|
+
unchanged: false,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Verify record was created
|
|
259
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
260
|
+
expect(records.find((r) => r.name === 'api')).toBeDefined();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should handle root domain records (@)', async () => {
|
|
264
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
265
|
+
{ name: '@', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
266
|
+
]);
|
|
267
|
+
|
|
268
|
+
expect(results).toHaveLength(1);
|
|
269
|
+
expect(results[0]?.created).toBe(true);
|
|
270
|
+
|
|
271
|
+
// Verify record was created at root
|
|
272
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
273
|
+
expect(records.find((r) => r.name === '@')).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should mark unchanged when record exists with same value', async () => {
|
|
277
|
+
// Create initial record (use UPSERT to handle idempotency)
|
|
278
|
+
await client.send(
|
|
279
|
+
new ChangeResourceRecordSetsCommand({
|
|
280
|
+
HostedZoneId: hostedZoneId,
|
|
281
|
+
ChangeBatch: {
|
|
282
|
+
Changes: [
|
|
283
|
+
{
|
|
284
|
+
Action: 'UPSERT',
|
|
285
|
+
ResourceRecordSet: {
|
|
286
|
+
Name: `api.${TEST_DOMAIN}`,
|
|
287
|
+
Type: 'A',
|
|
288
|
+
TTL: 300,
|
|
289
|
+
ResourceRecords: [{ Value: '1.2.3.4' }],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
298
|
+
{ name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
expect(results).toHaveLength(1);
|
|
302
|
+
expect(results[0]).toEqual({
|
|
303
|
+
record: { name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' },
|
|
304
|
+
created: false,
|
|
305
|
+
unchanged: true,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should update record when value changes', async () => {
|
|
310
|
+
// Create initial record (use UPSERT to handle idempotency)
|
|
311
|
+
await client.send(
|
|
312
|
+
new ChangeResourceRecordSetsCommand({
|
|
313
|
+
HostedZoneId: hostedZoneId,
|
|
314
|
+
ChangeBatch: {
|
|
315
|
+
Changes: [
|
|
316
|
+
{
|
|
317
|
+
Action: 'UPSERT',
|
|
318
|
+
ResourceRecordSet: {
|
|
319
|
+
Name: `api.${TEST_DOMAIN}`,
|
|
320
|
+
Type: 'A',
|
|
321
|
+
TTL: 300,
|
|
322
|
+
ResourceRecords: [{ Value: '1.2.3.4' }],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
331
|
+
{ name: 'api', type: 'A', ttl: 300, value: '5.6.7.8' },
|
|
332
|
+
]);
|
|
333
|
+
|
|
334
|
+
expect(results).toHaveLength(1);
|
|
335
|
+
expect(results[0]).toEqual({
|
|
336
|
+
record: { name: 'api', type: 'A', ttl: 300, value: '5.6.7.8' },
|
|
337
|
+
created: false,
|
|
338
|
+
unchanged: false,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Verify record was updated
|
|
342
|
+
const records = await provider.getRecords(TEST_DOMAIN);
|
|
343
|
+
const apiRecord = records.find((r) => r.name === 'api');
|
|
344
|
+
expect(apiRecord?.values[0]).toBe('5.6.7.8');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should handle multiple records with mixed states', async () => {
|
|
348
|
+
// Create initial record (use UPSERT to handle idempotency)
|
|
349
|
+
await client.send(
|
|
350
|
+
new ChangeResourceRecordSetsCommand({
|
|
351
|
+
HostedZoneId: hostedZoneId,
|
|
352
|
+
ChangeBatch: {
|
|
353
|
+
Changes: [
|
|
354
|
+
{
|
|
355
|
+
Action: 'UPSERT',
|
|
356
|
+
ResourceRecordSet: {
|
|
357
|
+
Name: `api.${TEST_DOMAIN}`,
|
|
358
|
+
Type: 'A',
|
|
359
|
+
TTL: 300,
|
|
360
|
+
ResourceRecords: [{ Value: '1.2.3.4' }],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const results = await provider.upsertRecords(TEST_DOMAIN, [
|
|
369
|
+
{ name: 'api', type: 'A', ttl: 300, value: '1.2.3.4' }, // Unchanged
|
|
370
|
+
{ name: 'www', type: 'A', ttl: 300, value: '1.2.3.4' }, // New
|
|
371
|
+
]);
|
|
372
|
+
|
|
373
|
+
expect(results).toHaveLength(2);
|
|
374
|
+
expect(results[0]?.unchanged).toBe(true);
|
|
375
|
+
expect(results[1]?.created).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe('hosted zone auto-detection', () => {
|
|
380
|
+
it('should auto-detect hosted zone from domain', async () => {
|
|
381
|
+
// Create provider without hostedZoneId
|
|
382
|
+
const autoProvider = new Route53Provider({
|
|
383
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const records = await autoProvider.getRecords(TEST_DOMAIN);
|
|
387
|
+
|
|
388
|
+
// Should work without error
|
|
389
|
+
expect(Array.isArray(records)).toBe(true);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should throw error when hosted zone not found', async () => {
|
|
393
|
+
const autoProvider = new Route53Provider({
|
|
394
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await expect(
|
|
398
|
+
autoProvider.getRecords('nonexistent-domain.com'),
|
|
399
|
+
).rejects.toThrow('No hosted zone found for domain');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeleteParameterCommand,
|
|
3
|
+
GetParameterCommand,
|
|
4
|
+
PutParameterCommand,
|
|
5
|
+
SSMClient,
|
|
6
|
+
} from '@aws-sdk/client-ssm';
|
|
7
|
+
import {
|
|
8
|
+
afterAll,
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeAll,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
it,
|
|
15
|
+
} from 'vitest';
|
|
16
|
+
import { SSMStateProvider } from '../SSMStateProvider';
|
|
17
|
+
import type { DokployStageState } from '../state';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* SSMStateProvider Tests
|
|
21
|
+
*
|
|
22
|
+
* These tests require LocalStack to be running with SSM enabled.
|
|
23
|
+
* Run: docker compose up -d localstack
|
|
24
|
+
*/
|
|
25
|
+
describe('SSMStateProvider', () => {
|
|
26
|
+
const LOCALSTACK_ENDPOINT = 'http://localhost:4566';
|
|
27
|
+
let client: SSMClient;
|
|
28
|
+
let provider: SSMStateProvider;
|
|
29
|
+
const workspaceName = 'test-workspace';
|
|
30
|
+
const testStage = 'test-stage';
|
|
31
|
+
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
process.env.AWS_ACCESS_KEY_ID = 'test';
|
|
34
|
+
process.env.AWS_SECRET_ACCESS_KEY = 'test';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
client = new SSMClient({
|
|
39
|
+
region: 'us-east-1',
|
|
40
|
+
endpoint: LOCALSTACK_ENDPOINT,
|
|
41
|
+
credentials: {
|
|
42
|
+
accessKeyId: 'test',
|
|
43
|
+
secretAccessKey: 'test',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
provider = new SSMStateProvider({
|
|
48
|
+
workspaceName,
|
|
49
|
+
region: 'us-east-1',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Override the client's endpoint for localstack
|
|
53
|
+
// @ts-expect-error - accessing private property for testing
|
|
54
|
+
provider.client = client;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
try {
|
|
59
|
+
await client.send(
|
|
60
|
+
new DeleteParameterCommand({
|
|
61
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore if parameter doesn't exist
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterAll(() => {
|
|
70
|
+
client.destroy();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('read', () => {
|
|
74
|
+
it('should return null when parameter does not exist', async () => {
|
|
75
|
+
const state = await provider.read('nonexistent-stage');
|
|
76
|
+
expect(state).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should read existing parameter', async () => {
|
|
80
|
+
const stateData: DokployStageState = {
|
|
81
|
+
provider: 'dokploy',
|
|
82
|
+
stage: testStage,
|
|
83
|
+
environmentId: 'env_123',
|
|
84
|
+
applications: { api: 'app_123' },
|
|
85
|
+
services: { postgresId: 'pg_123' },
|
|
86
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await client.send(
|
|
90
|
+
new PutParameterCommand({
|
|
91
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
92
|
+
Value: JSON.stringify(stateData),
|
|
93
|
+
Type: 'SecureString',
|
|
94
|
+
Overwrite: true,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const state = await provider.read(testStage);
|
|
99
|
+
expect(state).toEqual(stateData);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('write', () => {
|
|
104
|
+
it('should create new parameter', async () => {
|
|
105
|
+
const state: DokployStageState = {
|
|
106
|
+
provider: 'dokploy',
|
|
107
|
+
stage: testStage,
|
|
108
|
+
environmentId: 'env_123',
|
|
109
|
+
applications: { api: 'app_123' },
|
|
110
|
+
services: {},
|
|
111
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
await provider.write(testStage, state);
|
|
115
|
+
|
|
116
|
+
const response = await client.send(
|
|
117
|
+
new GetParameterCommand({
|
|
118
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
119
|
+
WithDecryption: true,
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const stored = JSON.parse(response.Parameter!.Value!);
|
|
124
|
+
expect(stored.applications).toEqual({ api: 'app_123' });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should update existing parameter', async () => {
|
|
128
|
+
const state1: DokployStageState = {
|
|
129
|
+
provider: 'dokploy',
|
|
130
|
+
stage: testStage,
|
|
131
|
+
environmentId: 'env_123',
|
|
132
|
+
applications: { api: 'app_old' },
|
|
133
|
+
services: {},
|
|
134
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const state2: DokployStageState = {
|
|
138
|
+
provider: 'dokploy',
|
|
139
|
+
stage: testStage,
|
|
140
|
+
environmentId: 'env_123',
|
|
141
|
+
applications: { api: 'app_new' },
|
|
142
|
+
services: {},
|
|
143
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await provider.write(testStage, state1);
|
|
147
|
+
await provider.write(testStage, state2);
|
|
148
|
+
|
|
149
|
+
const response = await client.send(
|
|
150
|
+
new GetParameterCommand({
|
|
151
|
+
Name: `/gkm/${workspaceName}/${testStage}/state`,
|
|
152
|
+
WithDecryption: true,
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const stored = JSON.parse(response.Parameter!.Value!);
|
|
157
|
+
expect(stored.applications.api).toBe('app_new');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should update lastDeployedAt timestamp', async () => {
|
|
161
|
+
const state: DokployStageState = {
|
|
162
|
+
provider: 'dokploy',
|
|
163
|
+
stage: testStage,
|
|
164
|
+
environmentId: 'env_123',
|
|
165
|
+
applications: {},
|
|
166
|
+
services: {},
|
|
167
|
+
lastDeployedAt: '2024-01-01T00:00:00.000Z',
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const originalTimestamp = state.lastDeployedAt;
|
|
171
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
172
|
+
await provider.write(testStage, state);
|
|
173
|
+
|
|
174
|
+
expect(state.lastDeployedAt).not.toBe(originalTimestamp);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
|
|
5
5
|
|
|
6
6
|
export function envParser(parser: SnifferEnvironmentParser) {
|
|
7
|
-
const
|
|
7
|
+
const _config = parser.create((get) => ({
|
|
8
8
|
port: get('PORT').string(),
|
|
9
9
|
apiKey: get('API_KEY').string(),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
12
|
// Throw after creating the parser but before returning
|
|
13
13
|
throw new Error('EnvParser initialization failed');
|
|
14
|
-
|
|
15
|
-
return config;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
export default envParser;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test endpoint with multiple services.
|
|
3
|
+
* getEnvironment() should return ['AUTH_SECRET', 'AUTH_URL', 'DATABASE_URL', 'DB_POOL_SIZE'].
|
|
4
|
+
*/
|
|
5
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { authService, databaseService } from '../services';
|
|
8
|
+
|
|
9
|
+
export const login = e
|
|
10
|
+
.services([databaseService, authService])
|
|
11
|
+
.post('/auth/login')
|
|
12
|
+
.body(z.object({ email: z.string(), password: z.string() }))
|
|
13
|
+
.output(z.object({ token: z.string() }))
|
|
14
|
+
.handle(async () => {
|
|
15
|
+
return { token: 'test-token' };
|
|
16
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test endpoint without any services.
|
|
3
|
+
* getEnvironment() should return [].
|
|
4
|
+
*/
|
|
5
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
export const healthCheck = e
|
|
9
|
+
.get('/health')
|
|
10
|
+
.output(z.object({ status: z.string() }))
|
|
11
|
+
.handle(async () => {
|
|
12
|
+
return { status: 'ok' };
|
|
13
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test endpoint with database service.
|
|
3
|
+
* getEnvironment() should return ['DATABASE_URL', 'DB_POOL_SIZE'].
|
|
4
|
+
*/
|
|
5
|
+
import { e } from '@geekmidas/constructs/endpoints';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { databaseService } from '../services';
|
|
8
|
+
|
|
9
|
+
export const getUsers = e
|
|
10
|
+
.services([databaseService])
|
|
11
|
+
.get('/users')
|
|
12
|
+
.output(z.array(z.object({ id: z.string(), name: z.string() })))
|
|
13
|
+
.handle(async () => {
|
|
14
|
+
return [{ id: '1', name: 'Test User' }];
|
|
15
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test services for route-based app sniffing fixtures.
|
|
3
|
+
* These services access environment variables via envParser.create().
|
|
4
|
+
*/
|
|
5
|
+
import type { EnvironmentParser } from '@geekmidas/envkit';
|
|
6
|
+
|
|
7
|
+
// Simple Service type for test fixtures (avoids importing @geekmidas/services)
|
|
8
|
+
type TestService<TName extends string, TInstance> = {
|
|
9
|
+
serviceName: TName;
|
|
10
|
+
register(ctx: {
|
|
11
|
+
envParser: EnvironmentParser<Record<string, string | undefined>>;
|
|
12
|
+
}): Promise<TInstance>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Database service - requires DATABASE_URL
|
|
16
|
+
export const databaseService: TestService<'database', { url: string }> = {
|
|
17
|
+
serviceName: 'database' as const,
|
|
18
|
+
async register({ envParser }) {
|
|
19
|
+
const config = envParser
|
|
20
|
+
.create((get) => ({
|
|
21
|
+
url: get('DATABASE_URL').string(),
|
|
22
|
+
poolSize: get('DB_POOL_SIZE').string().transform(Number).optional(),
|
|
23
|
+
}))
|
|
24
|
+
.parse();
|
|
25
|
+
return { url: config.url };
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Cache service - requires REDIS_URL
|
|
30
|
+
export const cacheService: TestService<'cache', { url: string }> = {
|
|
31
|
+
serviceName: 'cache' as const,
|
|
32
|
+
async register({ envParser }) {
|
|
33
|
+
const config = envParser
|
|
34
|
+
.create((get) => ({
|
|
35
|
+
url: get('REDIS_URL').string(),
|
|
36
|
+
}))
|
|
37
|
+
.parse();
|
|
38
|
+
return { url: config.url };
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Auth service - requires AUTH_SECRET and AUTH_URL
|
|
43
|
+
export const authService: TestService<'auth', { secret: string; url: string }> =
|
|
44
|
+
{
|
|
45
|
+
serviceName: 'auth' as const,
|
|
46
|
+
async register({ envParser }) {
|
|
47
|
+
const config = envParser
|
|
48
|
+
.create((get) => ({
|
|
49
|
+
secret: get('AUTH_SECRET').string(),
|
|
50
|
+
url: get('AUTH_URL').string(),
|
|
51
|
+
}))
|
|
52
|
+
.parse();
|
|
53
|
+
return { secret: config.secret, url: config.url };
|
|
54
|
+
},
|
|
55
|
+
};
|