@appliance.sh/api-server 1.14.0 → 1.16.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/package.json CHANGED
@@ -1,73 +1,46 @@
1
1
  {
2
2
  "name": "@appliance.sh/api-server",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
4
4
  "description": "",
5
5
  "author": "Eliot Lim",
6
6
  "repository": "https://github.com/appliance-sh/appliance.sh",
7
7
  "license": "MIT",
8
8
  "scripts": {
9
- "build": "nest build",
9
+ "build": "tsc",
10
10
  "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
- "start": "nest start",
12
- "start:dev": "nest start --watch",
13
- "start:debug": "nest start --debug --watch",
14
- "start:prod": "node dist/main",
11
+ "start": "node dist/src/main.js",
12
+ "start:dev": "tsx watch src/main.ts",
13
+ "start:debug": "tsx watch --inspect src/main.ts",
14
+ "start:prod": "node dist/src/main.js",
15
15
  "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16
- "test": "jest",
17
- "test:watch": "jest --watch",
18
- "test:cov": "jest --coverage",
19
- "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20
- "test:e2e": "jest --config ./test/jest-e2e.json"
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:cov": "vitest run --coverage",
19
+ "test:e2e": "vitest run --config vitest.e2e.config.ts"
21
20
  },
22
21
  "dependencies": {
23
- "@nestjs/common": "^11.0.1",
24
- "@nestjs/core": "^11.0.1",
25
- "@nestjs/platform-express": "^11.0.1",
26
- "@pulumi/aws": "^7.15.0",
27
- "@pulumi/pulumi": "^3.213.0",
28
- "reflect-metadata": "^0.2.2",
29
- "rxjs": "^7.8.1"
22
+ "@appliance.sh/infra": "1.16.0",
23
+ "@appliance.sh/sdk": "1.16.0",
24
+ "@pulumi/aws": "^7.16.0",
25
+ "@pulumi/aws-native": "^1.48.0",
26
+ "@pulumi/pulumi": "^3.216.0",
27
+ "express": "^5.2.1"
30
28
  },
31
29
  "devDependencies": {
32
30
  "@eslint/eslintrc": "^3.2.0",
33
31
  "@eslint/js": "^9.18.0",
34
- "@nestjs/cli": "^11.0.0",
35
- "@nestjs/schematics": "^11.0.0",
36
- "@nestjs/testing": "^11.0.1",
37
32
  "@types/express": "^5.0.0",
38
- "@types/jest": "^30.0.0",
39
33
  "@types/node": "^22.10.7",
40
34
  "@types/supertest": "^6.0.2",
41
35
  "eslint": "^9.18.0",
42
36
  "eslint-config-prettier": "^10.0.1",
43
37
  "eslint-plugin-prettier": "^5.2.2",
44
38
  "globals": "^16.0.0",
45
- "jest": "^30.0.0",
46
39
  "prettier": "^3.4.2",
47
- "source-map-support": "^0.5.21",
48
40
  "supertest": "^7.0.0",
49
- "ts-jest": "^29.2.5",
50
- "ts-loader": "^9.5.2",
51
- "ts-node": "^10.9.2",
52
- "tsconfig-paths": "^4.2.0",
41
+ "tsx": "^4.19.2",
53
42
  "typescript": "^5.7.3",
54
- "typescript-eslint": "^8.20.0"
55
- },
56
- "jest": {
57
- "moduleFileExtensions": [
58
- "js",
59
- "json",
60
- "ts"
61
- ],
62
- "rootDir": "src",
63
- "testRegex": ".*\\.spec\\.ts$",
64
- "transform": {
65
- "^.+\\.(t|j)s$": "ts-jest"
66
- },
67
- "collectCoverageFrom": [
68
- "**/*.(t|j)s"
69
- ],
70
- "coverageDirectory": "../coverage",
71
- "testEnvironment": "node"
43
+ "typescript-eslint": "^8.20.0",
44
+ "vitest": "^3.0.4"
72
45
  }
73
46
  }
@@ -1,22 +1,12 @@
1
- import { Test, TestingModule } from '@nestjs/testing';
2
- import { AppController } from './app.controller';
3
- import { AppService } from './app.service';
4
-
5
- describe('AppController', () => {
6
- let appController: AppController;
7
-
8
- beforeEach(async () => {
9
- const app: TestingModule = await Test.createTestingModule({
10
- controllers: [AppController],
11
- providers: [AppService],
12
- }).compile();
13
-
14
- appController = app.get<AppController>(AppController);
15
- });
16
-
17
- describe('root', () => {
18
- it('should return "Hello World!"', () => {
19
- expect(appController.getHello()).toBe('Hello World!');
20
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import request from 'supertest';
3
+ import { createApp } from './main';
4
+
5
+ describe('Index Route', () => {
6
+ it('should return "Hello World!"', async () => {
7
+ const app = createApp();
8
+ const response = await request(app).get('/');
9
+ expect(response.status).toBe(200);
10
+ expect(response.text).toBe('Hello World!');
21
11
  });
22
12
  });
package/src/main.ts CHANGED
@@ -1,8 +1,26 @@
1
- import { NestFactory } from '@nestjs/core';
2
- import { AppModule } from './app.module';
1
+ import express from 'express';
2
+ import { indexRoutes } from './routes';
3
+ import { infraRoutes } from './routes/infra';
4
+
5
+ export function createApp() {
6
+ const app = express();
7
+
8
+ app.use(express.json());
9
+
10
+ // Set up routes
11
+ app.use('/', indexRoutes);
12
+ app.use('/infra', infraRoutes);
13
+
14
+ return app;
15
+ }
3
16
 
4
17
  async function bootstrap() {
5
- const app = await NestFactory.create(AppModule);
6
- await app.listen(process.env.PORT ?? 3000);
18
+ const app = createApp();
19
+ const port = process.env.PORT ?? 3000;
20
+
21
+ app.listen(port, () => {
22
+ console.log(`Server is running on http://localhost:${port}`);
23
+ });
7
24
  }
25
+
8
26
  bootstrap();
@@ -0,0 +1,7 @@
1
+ import { Router } from 'express';
2
+
3
+ export const indexRoutes = Router();
4
+
5
+ indexRoutes.get('/', (_req, res) => {
6
+ res.send('Hello World!');
7
+ });
@@ -0,0 +1,24 @@
1
+ import { Router } from 'express';
2
+ import { pulumiService } from '../../services/pulumi.service';
3
+
4
+ export const infraRoutes = Router();
5
+
6
+ infraRoutes.post('/deploy', async (_req, res) => {
7
+ try {
8
+ const result = await pulumiService.deploy();
9
+ res.json(result);
10
+ } catch (error) {
11
+ console.error('Deploy error:', error);
12
+ res.status(500).json({ error: 'Deploy failed', message: String(error) });
13
+ }
14
+ });
15
+
16
+ infraRoutes.post('/destroy', async (_req, res) => {
17
+ try {
18
+ const result = await pulumiService.destroy();
19
+ res.json(result);
20
+ } catch (error) {
21
+ console.error('Destroy error:', error);
22
+ res.status(500).json({ error: 'Destroy failed', message: String(error) });
23
+ }
24
+ });
@@ -1,8 +1,7 @@
1
- import { Injectable, Logger } from '@nestjs/common';
2
1
  import * as auto from '@pulumi/pulumi/automation';
3
2
  import * as aws from '@pulumi/aws';
4
3
  import * as awsNative from '@pulumi/aws-native';
5
- import { ApplianceStack } from './ApplianceStack';
4
+ import { ApplianceStack } from '@appliance.sh/infra';
6
5
  import { applianceBaseConfig } from '@appliance.sh/sdk';
7
6
 
8
7
  export type PulumiAction = 'deploy' | 'destroy';
@@ -15,9 +14,7 @@ export interface PulumiResult {
15
14
  stackName: string;
16
15
  }
17
16
 
18
- @Injectable()
19
- export class PulumiService {
20
- private readonly logger = new Logger(PulumiService.name);
17
+ class PulumiService {
21
18
  private readonly projectName = 'appliance-api-managed-proj';
22
19
 
23
20
  private readonly baseConfig = process.env.APPLIANCE_BASE_CONFIG
@@ -108,7 +105,7 @@ export class PulumiService {
108
105
 
109
106
  async deploy(stackName = 'appliance-api-managed'): Promise<PulumiResult> {
110
107
  const stack = await this.getOrCreateStack(stackName);
111
- const result = await stack.up({ onOutput: (m) => this.logger.log(m) });
108
+ const result = await stack.up({ onOutput: (m) => console.log(m) });
112
109
  const changes = result.summary.resourceChanges || {};
113
110
  const totalChanges = Object.entries(changes)
114
111
  .filter(([k]) => k !== 'same')
@@ -126,7 +123,7 @@ export class PulumiService {
126
123
  async destroy(stackName = 'appliance-api-managed'): Promise<PulumiResult> {
127
124
  try {
128
125
  const stack = await this.selectExistingStack(stackName);
129
- await stack.destroy({ onOutput: (m) => this.logger.log(m) });
126
+ await stack.destroy({ onOutput: (m) => console.log(m) });
130
127
  return { action: 'destroy', ok: true, idempotentNoop: false, message: 'Stack resources deleted', stackName };
131
128
  } catch (e) {
132
129
  if (!(e instanceof Error)) throw e;
@@ -144,3 +141,6 @@ export class PulumiService {
144
141
  }
145
142
  }
146
143
  }
144
+
145
+ // Export a singleton instance
146
+ export const pulumiService = new PulumiService();
@@ -1,22 +1,18 @@
1
- import { Test, TestingModule } from '@nestjs/testing';
2
- import { INestApplication } from '@nestjs/common';
1
+ import { describe, it, expect, beforeAll } from 'vitest';
3
2
  import request from 'supertest';
4
- import { App } from 'supertest/types';
5
- import { AppModule } from '../src/app.module';
3
+ import { createApp } from '../src/main';
4
+ import type { Express } from 'express';
6
5
 
7
- describe('AppController (e2e)', () => {
8
- let app: INestApplication<App>;
6
+ describe('App (e2e)', () => {
7
+ let app: Express;
9
8
 
10
- beforeEach(async () => {
11
- const moduleFixture: TestingModule = await Test.createTestingModule({
12
- imports: [AppModule],
13
- }).compile();
14
-
15
- app = moduleFixture.createNestApplication();
16
- await app.init();
9
+ beforeAll(() => {
10
+ app = createApp();
17
11
  });
18
12
 
19
- it('/ (GET)', () => {
20
- return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!');
13
+ it('/ (GET)', async () => {
14
+ const response = await request(app).get('/');
15
+ expect(response.status).toBe(200);
16
+ expect(response.text).toBe('Hello World!');
21
17
  });
22
18
  });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.spec.ts'],
6
+ globals: false,
7
+ },
8
+ });
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['test/**/*.e2e-spec.ts'],
6
+ globals: false,
7
+ },
8
+ });
package/nest-cli.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/nest-cli",
3
- "collection": "@nestjs/schematics",
4
- "sourceRoot": "src",
5
- "compilerOptions": {
6
- "deleteOutDir": true
7
- }
8
- }
@@ -1,12 +0,0 @@
1
- import { Controller, Get } from '@nestjs/common';
2
- import { AppService } from './app.service';
3
-
4
- @Controller()
5
- export class AppController {
6
- constructor(private readonly appService: AppService) {}
7
-
8
- @Get()
9
- getHello(): string {
10
- return this.appService.getHello();
11
- }
12
- }
package/src/app.module.ts DELETED
@@ -1,11 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { AppController } from './app.controller';
3
- import { AppService } from './app.service';
4
- import { PulumiModule } from './pulumi/pulumi.module';
5
-
6
- @Module({
7
- imports: [PulumiModule],
8
- controllers: [AppController],
9
- providers: [AppService],
10
- })
11
- export class AppModule {}
@@ -1,8 +0,0 @@
1
- import { Injectable } from '@nestjs/common';
2
-
3
- @Injectable()
4
- export class AppService {
5
- getHello(): string {
6
- return 'Hello World!';
7
- }
8
- }
@@ -1,164 +0,0 @@
1
- import * as pulumi from '@pulumi/pulumi';
2
- import * as aws from '@pulumi/aws';
3
- import * as awsNative from '@pulumi/aws-native';
4
- import { ApplianceBaseConfig } from '@appliance.sh/sdk';
5
-
6
- export interface ApplianceStackArgs {
7
- tags?: Record<string, string>;
8
- config: ApplianceBaseConfig;
9
- }
10
-
11
- export interface ApplianceStackOpts extends pulumi.ComponentResourceOptions {
12
- globalProvider: aws.Provider;
13
- provider: aws.Provider;
14
- nativeProvider: awsNative.Provider;
15
- nativeGlobalProvider: awsNative.Provider;
16
- }
17
-
18
- export class ApplianceStack extends pulumi.ComponentResource {
19
- lambdaRole: aws.iam.Role;
20
- lambdaRolePolicy: aws.iam.Policy;
21
- lambda: aws.lambda.Function;
22
- lambdaUrl: aws.lambda.FunctionUrl;
23
- dnsRecord: pulumi.Output<string>;
24
-
25
- constructor(name: string, args: ApplianceStackArgs, opts: ApplianceStackOpts) {
26
- super('appliance:aws:ApplianceStack', name, args, opts);
27
-
28
- const defaultOpts = { parent: this, provider: opts.provider };
29
- const defaultNativeOpts = { parent: this, provider: opts.nativeProvider };
30
- const defaultTags = { stack: name, managed: 'appliance', ...args.tags };
31
-
32
- this.lambdaRole = new aws.iam.Role(`${name}-role`, {
33
- path: `/appliance/${name}/`,
34
- assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ Service: 'lambda.amazonaws.com' }),
35
- tags: defaultTags,
36
- });
37
-
38
- this.lambdaRolePolicy = new aws.iam.Policy(`${name}-policy`, {
39
- path: `/appliance/${name}/`,
40
- policy: {
41
- Version: '2012-10-17',
42
- Statement: [{ Effect: 'Allow', Action: 'logs:CreateLogGroup', Resource: '*' }],
43
- },
44
- });
45
-
46
- new aws.iam.RolePolicyAttachment(`${name}-role-policy-attachment`, {
47
- role: this.lambdaRole.name,
48
- policyArn: this.lambdaRolePolicy.arn,
49
- });
50
-
51
- this.lambda = new aws.lambda.CallbackFunction(
52
- `${name}-handler`,
53
- {
54
- runtime: 'nodejs22.x',
55
- callback: async () => {
56
- return { statusCode: 200, body: JSON.stringify({ message: 'Hello world!' }) };
57
- },
58
- tags: defaultTags,
59
- },
60
- defaultOpts
61
- );
62
-
63
- // lambda url
64
- this.lambdaUrl = new aws.lambda.FunctionUrl(
65
- `${name}-url`,
66
- {
67
- functionName: this.lambda.name,
68
- authorizationType: args.config.aws.cloudfrontDistributionId ? 'AWS_IAM' : 'NONE',
69
- },
70
- defaultOpts
71
- );
72
-
73
- this.dnsRecord = pulumi.interpolate`${name}.${args.config.domainName ?? ''}`;
74
-
75
- if (args.config.aws.cloudfrontDistributionId) {
76
- new aws.lambda.Permission(`${name}-url-invoke-url-permission`, {
77
- function: this.lambda.name,
78
- action: 'lambda:InvokeFunctionUrl',
79
- principal: 'cloudfront.amazonaws.com',
80
- functionUrlAuthType: 'AWS_IAM',
81
- sourceArn: pulumi.interpolate`arn:aws:cloudfront::${
82
- aws.getCallerIdentityOutput({}, { provider: opts.provider }).accountId
83
- }:distribution/${args.config.aws.cloudfrontDistributionId}`,
84
- statementId: 'FunctionURLAllowCloudFrontAccess',
85
- });
86
-
87
- // Grant the edge router role permission to invoke the Lambda Function URL
88
- // The edge router role is the execution role of the Lambda@Edge function that signs requests
89
- if (args.config.aws.edgeRouterRoleArn) {
90
- new aws.lambda.Permission(`${name}-invoke-url-edge-router-permission`, {
91
- function: this.lambda.name,
92
- action: 'lambda:InvokeFunctionUrl',
93
- principal: args.config.aws.edgeRouterRoleArn,
94
- functionUrlAuthType: 'AWS_IAM',
95
- statementId: 'FunctionURLAllowEdgeRouterRoleAccess',
96
- });
97
-
98
- new awsNative.lambda.Permission(
99
- `${name}-invoke-edge-router-permission`,
100
- {
101
- action: 'lambda:InvokeFunction',
102
- principal: args.config.aws.edgeRouterRoleArn,
103
- functionName: this.lambda.name,
104
- invokedViaFunctionUrl: true,
105
- },
106
- defaultNativeOpts
107
- );
108
- }
109
- } else {
110
- new aws.lambda.Permission(`${name}-url-invoke-url-permission`, {
111
- function: this.lambda.name,
112
- action: 'lambda:InvokeFunctionUrl',
113
- principal: '*',
114
- functionUrlAuthType: 'NONE',
115
- statementId: 'FunctionURLAllowPublicAccess',
116
- });
117
- }
118
-
119
- if (args.config.aws.cloudfrontDistributionId && args.config.aws.cloudfrontDistributionDomainName) {
120
- new awsNative.lambda.Permission(
121
- `${name}-url-invoke-lambda-native-permission`,
122
- {
123
- action: 'lambda:InvokeFunction',
124
- principal: 'cloudfront.amazonaws.com',
125
- sourceArn: pulumi.interpolate`arn:aws:cloudfront::${
126
- aws.getCallerIdentityOutput({}, { provider: opts.provider }).accountId
127
- }:distribution/${args.config.aws.cloudfrontDistributionId}`,
128
- functionName: this.lambda.name,
129
- invokedViaFunctionUrl: true,
130
- },
131
- defaultNativeOpts
132
- );
133
-
134
- new aws.route53.Record(
135
- `${name}-cname-record`,
136
- {
137
- zoneId: args.config.aws.zoneId,
138
- name: pulumi.interpolate`${name}.${args.config.domainName ?? ''}`,
139
- type: 'CNAME',
140
- ttl: 60,
141
- records: [args.config.aws.cloudfrontDistributionDomainName],
142
- },
143
- { parent: this, provider: opts.globalProvider }
144
- );
145
-
146
- new aws.route53.Record(
147
- `${name}-txt-record`,
148
- {
149
- zoneId: args.config.aws.zoneId,
150
- name: pulumi.interpolate`origin.${name}.${args.config.domainName ?? ''}`,
151
- type: 'TXT',
152
- ttl: 60,
153
- records: [this.lambdaUrl.functionUrl],
154
- },
155
- { parent: this, provider: opts.globalProvider }
156
- );
157
- }
158
-
159
- this.registerOutputs({
160
- lambda: this.lambda,
161
- lambdaUrl: this.lambdaUrl,
162
- });
163
- }
164
- }
@@ -1,19 +0,0 @@
1
- import { Controller, HttpCode, Post } from '@nestjs/common';
2
- import { PulumiService } from './pulumi.service';
3
-
4
- @Controller('infra')
5
- export class PulumiController {
6
- constructor(private readonly pulumi: PulumiService) {}
7
-
8
- @Post('deploy')
9
- @HttpCode(200)
10
- async deploy() {
11
- return await this.pulumi.deploy();
12
- }
13
-
14
- @Post('destroy')
15
- @HttpCode(200)
16
- async destroy() {
17
- return await this.pulumi.destroy();
18
- }
19
- }
@@ -1,9 +0,0 @@
1
- import { Module } from '@nestjs/common';
2
- import { PulumiService } from './pulumi.service';
3
- import { PulumiController } from './pulumi.controller';
4
-
5
- @Module({
6
- providers: [PulumiService],
7
- controllers: [PulumiController],
8
- })
9
- export class PulumiModule {}
@@ -1,9 +0,0 @@
1
- {
2
- "moduleFileExtensions": ["js", "json", "ts"],
3
- "rootDir": ".",
4
- "testEnvironment": "node",
5
- "testRegex": ".e2e-spec.ts$",
6
- "transform": {
7
- "^.+\\.(t|j)s$": "ts-jest"
8
- }
9
- }