How does one integrate DynamoDB Local into the development process when using aws-cdk? For example, the following app (using the "sample app" template) works, but how would one run sam local invoke <id of the backendHandler lambda> and point to a DynamoDB Local instance running on your machine (instead of having to run cdk deploy) to see in action?
// lib/minimal-app-stack.ts
import * as apiGateway from "#aws-cdk/aws-apigateway";
import * as cdk from "#aws-cdk/core";
import {MinimalBackend} from "./minimal-backend";
export class MinimalAppStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const minimalBackend = new MinimalBackend(this, "MinimalBackend");
new apiGateway.LambdaRestApi(this, "Endpoint", {
handler: minimalBackend.handler
});
}
}
// lib/minimal-backend.ts
import * as cdk from "#aws-cdk/core";
import * as dynamodb from "#aws-cdk/aws-dynamodb";
import * as lambda from "#aws-cdk/aws-lambda";
export class MinimalBackend extends cdk.Construct {
public readonly handler: lambda.Function;
constructor(scope: cdk.Construct, id: string) {
super(scope, id);
const minimalTable = new dynamodb.Table(this, "MinimalTable", {
partitionKey: {
name: "id",
type: dynamodb.AttributeType.STRING
}
});
this.handler = new lambda.Function(this, "BackendHandler", {
code: lambda.Code.fromAsset("lambda"),
handler: "backendHandler.handler",
runtime: lambda.Runtime.NODEJS_12_X,
environment: {
TABLE_NAME: minimalTable.tableName
}
});
minimalTable.grantReadWriteData(this.handler);
}
}
// lambda/backendHandler.ts
// #ts-ignore
/// <reference types="aws-sdk" />
import AWS = require("aws-sdk");
const tableName = process.env.TABLE_NAME || "";
const dynamo = new AWS.DynamoDB.DocumentClient();
async function getAllItems() {
const scanResult = await dynamo
.scan({
TableName: tableName
})
.promise();
return scanResult;
}
exports.handler = async function (event: AWSLambda.APIGatewayEvent) {
const response = await getAllItems();
const data = response.Items || [];
const stringifiedData = JSON.stringify(data, undefined, 2);
return {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "OPTIONS,GET,POST,DELETE"
},
body: stringifiedData
}
};
If I was inside a plain node app, I could access my DynamoDB Local instance with:
const docClient = new AWS.DynamoDB.DocumentClient( {
region: "us-west-2",
endpoint: "http://localhost:8000",
convertEmptyValues: true
});
Not sure how to integrate that into aws-cdk though. Any ideas?
Related
import { DataTypes } from "sequelize";
import dbConnect from "./dbConnect";
async function UserModel() {
const sequelize = await dbConnect();
const User = sequelize.define(
"User",
{
userKey: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
name: {
type: DataTypes.STRING(50),
allowNull: false,
},
username: {
type: DataTypes.STRING(50),
allowNull: false,
},
},
{
freezeTableName: true, // enforces that table name = model name
}
);
return User;
}
export default UserModel;
The above module returns the UserModel correctly, but I need to resolve it twice --
const users = await (await User()).findAll();
-- in the module that imports it b/c I'm calling a method that also returns a promise.
I'd like to call it like this --
const users = await User().findAll();
-- but everything I've tried has failed.
See usage here:
export async function getServerSideProps() {
const users = await (await User()).findAll();
return {
props: {
users,
},
};
}
Can you shed some light on how to do this?
I'd like to pass a authorized Context type while I am doing a mutation, but how can I do it with nexus.js?
./src/types/Context.ts
import { PrismaClient } from "#prisma/client";
export interface Context {
uid?: string | null;
prisma: PrismaClient;
}
export interface AuthorizedContext extends Context {
uid: string;
}
create Context function which is passed to Apollo Server
import prisma from "./prisma";
import { NextApiRequest } from "next";
import { loadIdToken } from "src/auth/firebaseAdmin";
import { Context } from "src/types/Context";
import { getAuth } from "firebase/auth";
export async function createContext({
req,
}: {
req: NextApiRequest;
}): Promise<Context> {
const uid = await loadIdToken(req);
const user = getAuth();
if (user.currentUser?.uid === null) {
console.log("user not logged in");
return { prisma };
}
return {
uid,
prisma,
};
}
This schema below is also passed to Apollo Server
src/graphql/schema.ts
import { makeSchema } from "nexus";
import { join } from "path";
import * as types from "./types";
const schema = makeSchema({
types,
contextType: {
module: join(process.cwd(), "./src/types/Context.ts"),
export: "Context",
},
outputs: {
schema: join(process.cwd(), "./generated/schema.graphql"),
typegen: join(process.cwd(), "./generated/nexus-typegen.d.ts"),
},
});
export { schema };
nexus mutation:
export const createCoalDepot = extendType({
type: "Mutation",
definition: (t) => {
t.field("createCoalDepot", {
type: CoalDepot,
args: { input: nonNull(CoalDepotInput), ctx: // pass AuthorizedContext Type here to use it in createCoalDepotResolver },
resolve: createCoalDepotResolver,
});
},
});
Here is the resolver where I got a error (1) got an userId: string | null | undefined Type 'string | null | undefined' is not assignable to type 'string'.
src/graphql/resolvers/coalDepotResolver.ts
export const createCoalDepotResolver: FieldResolver<
"Mutation",
"createCoalDepot"
> = async (_, { input }, { uid, prisma }) => {
const newCoalDepot = await prisma.coalDepot.create({
data: {
address: input.address,
image: input.image,
latitude: input.coordinates?.latitude,
longitude: input.coordinates?.longitude,
coalDepotName: input.coalDepotName,
mobilePhone: input.mobilePhone,
landline: input.landline,
coalDescAndAmount: input.coalDescAndAmount,
userId: uid, // the error (1)
},
});
};
I am building a multi-tenant NextJS app that uses next-auth for account authentication, tRPC for API's, and postgresql for a data store.
I am trying to find a way to dynamically update/set/mutate a session value based on some client-side interaction
The approach I am taking is similar to the one described in this article:
a User is granted access to an Organization through a Membership
a User may have a Membership to >1 Organization
a User can change which Organization they are "logged in" to through some client-side UI.
When the user authenticates, I want to:
set session.user.orgId to some orgId (if they belong to an org)
When the user changes the org they are accessing through some client-side UI, I want to:
update session.user.orgId = newOrgId (validating they have proper permissions before doing so, of course).
I have searched the net for ways to update/mutate session values, and as far as I can tell, it's only possible using next-auth's callbacks:
...
callbacks: {
async session({ session, user, token }) {
// we can modify session here, i.e `session.orgId = 'blah'`
// or look up a value in the db and attach it here.
return session
},
...
}
However, there is no clear way to trigger this update from the client, outside of the authentication flow. I.E, if the user clicks to change their org in some UI, how do I validate the change + update the session value, without requiring the user to re-authenticate?
Hack into NextAuth's PrismaAdapter and trpc's createContext.
For example:
File: src/pages/api/auth/[...nextauth].ts
import NextAuth, { Awaitable, type NextAuthOptions } from "next-auth";
import { PrismaAdapter } from "#next-auth/prisma-adapter";
import type { AdapterSession, AdapterUser } from "next-auth/adapters";
import { prisma } from "../../../server/db/client";
import { MembershipRole } from "#prisma/client";
...
const adapter = PrismaAdapter(prisma);
adapter.createSession = (session: {
sessionToken: string;
userId: string;
expires: Date;
}): Awaitable<AdapterSession> => {
return prisma.user
.findUniqueOrThrow({
where: {
id: session.userId,
},
select: {
memberships: {
where: {
isActiveOrg: true,
},
select: {
role: true,
organization: true,
},
},
},
})
.then((userWithOrg) => {
const membership = userWithOrg.memberships[0];
const orgId = membership?.organization.id;
return prisma.session.create({
data: {
expires: session.expires,
sessionToken: session.sessionToken,
user: {
connect: { id: session.userId },
},
organization: {
connect: {
id: orgId,
},
},
role: membership?.role as MembershipRole,
},
});
});
};
// the authOptions to user with NextAuth
export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
adapter: adapter,
// Configure one or more authentication providers
providers: [
...
],
};
export default NextAuth(authOptions);
File: src/server/trpc/context.ts
import type { inferAsyncReturnType } from "#trpc/server";
import type { CreateNextContextOptions } from "#trpc/server/adapters/next";
import type { Session } from "next-auth";
import { getServerAuthSession } from "../common/get-server-auth-session";
import { prisma } from "../db/client";
type CreateContextOptions = {
session: Session | null;
};
/** Use this helper for:
* - testing, so we dont have to mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
**/
export const createContextInner = async (opts: CreateContextOptions) => {
return {
session: opts.session,
prisma,
};
};
/**
* This is the actual context you'll use in your router
* #link https://trpc.io/docs/context
**/
export const createContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
const sessionToken = req.cookies["next-auth.session-token"];
const prismaSession = await prisma.session.findUniqueOrThrow({
where: {
sessionToken: sessionToken,
},
select: {
orgId: true,
},
});
const orgId = prismaSession.orgId;
// Get the session from the server using the unstable_getServerSession wrapper function
const session = (await getServerAuthSession({ req, res })) as Session;
const sessionWithOrg = {
session: {
user: {
// need this otherwise createContextInner doesn't accept for a possible null session.user.id
id: session?.user?.id || "",
orgId: orgId,
...session?.user,
},
expires: session?.expires,
},
};
const context = await createContextInner(sessionWithOrg);
return context;
};
export type Context = inferAsyncReturnType<typeof createContext>;
I'm trying to register a user and I get an error:
[uncaught application error]: TypeError - Cannot read properties of undefined (reading 'where')
Here is the code:
async register(context: any) {
const body = JSON.parse(await context.request.body().value);
const existing = await Users.where("email", body.email).get();
if (existing.length) {
context.response.status = 400;
return (context.response.body = { message: "User already exists" });
}
const hashedPassword = await Users.hashPassword(body.password);
const user = await Users.create({
email: body.email,
hashedPassword,
});
context.response.body = { message: "User created" };
}
Here is my model:
// import { Model, DataTypes } from "https://deno.land/x/denodb/mod.ts";
import { DataTypes, Model } from "https://deno.land/x/denodb/mod.ts";
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";
import {
create,
getNumericDate,
verify,
} from "https://deno.land/x/djwt/mod.ts";
import { JwtConfig } from "../middleware/jwt.ts";
import { db } from "../db.ts";
class Users extends Model {
static table = "users";
static timestamps = true;
static fields = {
id: {
primaryKey: true,
type: DataTypes.STRING,
},
email: {
type: DataTypes.STRING,
unique: true,
},
hashedPassword: {
type: DataTypes.STRING,
},
};
static defaults = {
id: crypto.randomUUID(),
};
// ...
static async hashPassword(password: string) {
const salt = await bcrypt.genSalt(8);
return bcrypt.hash(password, salt);
}
static generateJwt(id: string) {
// Create the payload with the expiration date (token have an expiry date) and the id of current user (you can add that you want)
const payload = {
id,
iat: getNumericDate(new Date()),
};
// return the generated token
return create({ alg: "HS512", typ: "JWT" }, payload, JwtConfig.secretKey);
}
}
//db.link([Users]);
//await db.sync();
export default Users;
Had to uncomment this:
db.link([Users]);
I have page with getServerSideProps:
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const apolloClient = initializeApollo()
await apolloClient.query({
query: UserQuery,
variables: { id: Number(ctx.query.id) },
})
return {
props: { initialApolloState: apolloClient.cache.extract() },
}
}
And resolver for User:
resolve: async (_parent, { where }, ctx, info) => {
const select = new PrismaSelect(info).value
console.log(ctx) // {} why is empty when i use getServerSideProps?
const res = await ctx.db.user.findOne({
where: { id: 1 },
...select,
})
return null
},
Same client side query works fine, but for some reason when i use getServerSideProps ctx is empty. How to fix this?
I can pass ctx to const apolloClient = initializeApollo(null, ctx), but it's will not run apolloServer ctx resolver function with db property and ctx.db will be undefined.
Apollo-server:
const apolloServer = new ApolloServer({
schema,
async context(ctx: Ctx): Promise<Ctx> {
ctx.db = prisma
return ctx
},
})
export const apolloServerHandler = apolloServer.createHandler({ path: '/api/graphql' })
Apollo-client:
import { useMemo } from 'react'
import { ApolloClient, InMemoryCache, NormalizedCacheObject } from '#apollo/client'
import { IncomingMessage, ServerResponse } from 'http'
type ResolverContext = {
req?: IncomingMessage
res?: ServerResponse
}
const typePolicies = {
Test: {
keyFields: ['id'],
},
User: {
keyFields: ['id'],
},
}
let apolloClient: ApolloClient<NormalizedCacheObject>
function createIsomorphLink(context: ResolverContext = {}): any {
if (typeof window === 'undefined') {
const { SchemaLink } = require('#apollo/client/link/schema')
const { schema } = require('backend/graphql/schema')
return new SchemaLink({ schema, context })
} else {
const { HttpLink } = require('#apollo/client/link/http')
return new HttpLink({
uri: '/api/graphql',
credentials: 'same-origin',
})
}
}
function createApolloClient(context?: ResolverContext): ApolloClient<NormalizedCacheObject> {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(context),
cache: new InMemoryCache({ typePolicies }),
})
}
export function initializeApollo(
initialState: any = null,
// Pages with Next.js data fetching methods, like `getStaticProps`, can send
// a custom context which will be used by `SchemaLink` to server render pages
context?: ResolverContext
): ApolloClient<NormalizedCacheObject> {
const _apolloClient = apolloClient ?? createApolloClient(context)
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// get hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
// Restore the cache using the data passed from getStaticProps/getServerSideProps
// combined with the existing cached data
_apolloClient.cache.restore({ ...existingCache, ...initialState })
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
// eslint-disable-next-line
export function useApollo(initialState: any): ApolloClient<NormalizedCacheObject> {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
You need to aplly context resolver:
async contextResolver(ctx: Ctx): Promise<Ctx> {
ctx.db = prisma
return ctx
},
const apolloServer = new ApolloServer({
schema,
context: contextResolver,
})
Inside getServerSideProps:
export const getServerSideProps: GetServerSideProps = async (ctx) => {
await contextResolver(ctx)
const apolloClient = initializeApollo(null, ctx)
await apolloClient.query({
query: UserQuery,
variables: { id: Number(ctx.query.id) },
})
return {
props: { initialApolloState: apolloClient.cache.extract() },
}
}