sst / ion

SST v3
https://sst.dev
MIT License
1.83k stars 220 forks source link

Resource is empty #1076

Open joneslloyd opened 1 week ago

joneslloyd commented 1 week ago

I am using a monorepo setup with Drizzle, and have encountered a peculiar problem whereby Resource is empty in seemingly all contexts.

I have this folder structure:

root
├── infra
│   ├── graphql
│   ├── migrations
│   ├── api.ts
│   ├── graphql-api.ts
│   ├── postgres-database.ts
│   ├── storage.ts
│   └── vpc.ts
├── packages
│   ├── core
│   │   └── src
│   │       ├── job
│   │       ├── recipient
│   │       ├── source
│   │       ├── summary
│   │       └── user
│   ├── functions
│   │   └── src
│   └── scripts
│       └── src
│           └── build-schema.ts

My build-schema.ts file (to create GraphQL Schema) is in the scripts subfolder under packages and looks like this:

import { buildSchema } from 'type-graphql';
import { writeFileSync } from 'fs';
import { printSchema } from 'graphql';
import * as path from 'path';
import { UserResolver } from '@test-project/core/user/resolver';
import { SourceResolver } from '@test-project/core/source/resolver';
import { RecipientResolver } from '@test-project/core/recipient/resolver';
import { SummaryResolver } from '@test-project/core/summary/resolver';
import { JobResolver } from '@test-project/core/job/resolver';

async function generateSchema() {
  const schema = await buildSchema({
    resolvers: [
      UserResolver,
      SourceResolver,
      RecipientResolver,
      SummaryResolver,
      JobResolver,
    ],
  });

  // Save the schema to a file
  const printedSchema = printSchema(schema);
  const filePath = path.join(__dirname, '..', '..', '..', 'infra', 'graphql', 'schema.graphql');
  writeFileSync(filePath, printedSchema);
  console.log('🟢 GraphQL schema successfully generated!');
}

generateSchema().catch((err) => {
  console.error("🔴 Error generating schema:", err);
  process.exit(1);
});

It takes a Resolver class for each of my entities (I'm trying to stick to DDD 😉), and generates the GraphQL Schema – A short snippet from UserResolver:

@Resolver()
export class UserResolver {
  private userRepository = new UserRepository();

  @Query(() => User, { nullable: true })
  async getUserById(@Arg("id", () => Int) id: number): Promise<User | null> {
    const user = await this.userRepository.getUserById(id);
    return user;
  }
}

This in turn uses the UserRepository class, which is just an abstraction layer over Drizzle (in case I ever want to replace Drizzle).

The problem is that the UserRepository imports db from my Drizzle file, like so:

import { Resource } from "sst";
import { drizzle } from "drizzle-orm/aws-data-api/pg";
import { RDSDataClient } from "@aws-sdk/client-rds-data";

const client = new RDSDataClient({});

export const db = drizzle(client, {
    database: Resource.TestProjectDb.database,
    secretArn: Resource.TestProjectDb.secretArn,
    resourceArn: Resource.TestProjectDb.clusterArn,
});

But when I run (in the packages/scripts/ folder): npm run sst shell ts-node ./src/build-schema.ts (in parallel to NO_BUN=true npx sst dev in another terminal window) I get the following error:

> sst shell ts-node ./src/build-schema.ts

/Users/joneslloyd/Dropbox/Mac/Documents/personal/development/test-project/node_modules/ts-node/src/index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript:
../core/src/drizzle.ts:8:22 - error TS2339: Property 'TestProjectDb' does not exist on type 'Resource'.

8   database: Resource.TestProjectDb.database,
                       ~~~~~~~~~~~~~
../core/src/drizzle.ts:9:23 - error TS2339: Property 'TestProjectDb' does not exist on type 'Resource'.

9   secretArn: Resource.TestProjectDb.secretArn,
                        ~~~~~~~~~~~~~
../core/src/drizzle.ts:10:25 - error TS2339: Property 'TestProjectDb' does not exist on type 'Resource'.

10   resourceArn: Resource.TestProjectDb.clusterArn,

The only way around this issue (which feels like a horrible hack) is to do:

import { Resource } from "sst";
import { drizzle } from "drizzle-orm/aws-data-api/pg";
import { RDSDataClient } from "@aws-sdk/client-rds-data";

const client = new RDSDataClient({});

const dbConfig = JSON.parse(process?.env?.SST_RESOURCE_TestProject || '{}');

export const db = drizzle(client, {
    database: dbConfig.database,
    secretArn: dbConfig.secretArn,
    resourceArn: dbConfig.clusterArn,
});

Do you know why this is happening?

It also occurs when I try to reference Resource in my ./infra/graphql-api.ts file like so:

import { Resource } from "sst";
import { rds, secret } from "./postgres-database";

export const api = new sst.aws.AppSync("TestProjectApi", {
    schema: "./infra/graphql/schema.graphql",
});
console.log({Resource});
api.addDataSource({
    name: "LambdaDataSource",
    lambda: {
    link: [
        rds,
        secret,
    ],
    handler: "./packages/functions/src/api.handler",
    environment: {
        DATABASE_SECRET_ARN: Resource.TestProjectDb.secretArn,
        DATABASE_CLUSTER_ARN: Resource.TestProjectDb.clusterArn,
        DATABASE_NAME: Resource.TestProjectDb.database,
    }
},
});

(Resource is empty).

The file in which TestProjectDb is defined is ./infra/postgres-database.ts and looks like this:

import { vpc } from "./vpc";

export const rds = new sst.aws.Postgres("TestProjectDb", {
    vpc
});

export const secret = new sst.Secret("DBCredentialsSecret", "dbadmin");

And my sst.config.ts looks like this:

/// <reference path="./.sst/platform/config.d.ts" />

export default $config({
  app(input) {
    return {
      name: "test-project",
      removal: input?.stage === "production" ? "retain" : "remove",
      home: "aws",
      providers: {
        aws: {
          version: "6.41.0"
        }
      }
    };
  },
  async run() {
    await import("./infra/vpc");
    await import("./infra/storage");
    await import("./infra/postgres-database");
    const graphqlApiFile = await import("./infra/graphql-api");

    return {
      graphqlApi: graphqlApiFile.api.url
    };
  },
});

My sst-env.d.ts looks like:

/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
import "sst"
export {}
declare module "sst" {
  export interface Resource {
    "DBCredentialsSecret": {
      "type": "sst.sst.Secret"
      "value": string
    }
    "TestProjectApi": {
      "type": "sst.aws.AppSync"
      "url": string
    }
    "TestProjectApiDataSourceLambdaDataSourceFunction": {
      "name": string
      "type": "sst.aws.Function"
    }
    "TestProjectBucket": {
      "name": string
      "type": "sst.aws.Bucket"
    }
    "TestProjectDb": {
      "clusterArn": string
      "database": string
      "host": string
      "password": string
      "port": number
      "secretArn": string
      "type": "sst.aws.Postgres"
      "username": string
    }
    "TestProjectVpc": {
      "type": "sst.aws.Vpc"
    }
  }
}

And when I run NO_BUN=true npx sst dev env (or just npx sst dev env) I get Unexpected error occurred. Please check the logs in .sst/log/sst.log, which itself contains nothing more than:

time=2024-09-16T22:28:51.565+02:00 level=ERROR msg="exited with error" err="exit status 1"

I am using sst version 3.1.11 as a dependency in both the main package.json and all others.

Any help here would be appreciated!

lucasATeixeira commented 1 day ago

I had the same error ("does not exist on type Resource") and the problem was that my sst-env.d.ts was not included to tsconfig, so typescript could not map Resources types..

I Had something like that:

{
  "include": ["src"],
  "exclude": ["node_modules", ".sst"]
}

And that was causing the error.

Try including sst.env.d.ts to tsconfig.json

joneslloyd commented 21 hours ago

Unfortunately including sst-env.d.ts file in tsconfig.json doesn't help (plus per the aws-monorepo example, the root tsconfig.json is normally empty).

Thanks for the suggestion though.