├── 2022-08-02-practical-graphql-with-pothos ├── .env ├── .gitignore ├── .vscode │ └── launch.json ├── README.md ├── graphql │ ├── genql │ │ ├── guards.esm.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── schema.graphql │ │ ├── schema.ts │ │ └── types.esm.js │ ├── package.json │ └── schema.graphql ├── package.json ├── services │ ├── core │ │ ├── article.ts │ │ ├── dynamo.ts │ │ ├── task.ts │ │ └── user.ts │ ├── functions │ │ └── graphql │ │ │ ├── builder.ts │ │ │ ├── graphql.ts │ │ │ ├── schema.ts │ │ │ └── types │ │ │ ├── article.ts │ │ │ └── project.ts │ ├── package.json │ └── tsconfig.json ├── sst.json ├── stacks │ ├── Api.ts │ ├── Database.ts │ ├── Web.ts │ └── index.ts ├── tsconfig.json ├── vitest.config.ts ├── web │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ ├── pages │ │ │ └── Article.tsx │ │ ├── sst-env.d.ts │ │ ├── urql.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── yarn.lock ├── 2022-08-09-trpc ├── .gitignore ├── package.json ├── services │ ├── functions │ │ ├── lambda.ts │ │ └── trpc.ts │ ├── package.json │ ├── test │ │ └── sample.test.ts │ └── tsconfig.json ├── sst.json ├── stacks │ ├── MyStack.ts │ └── index.ts ├── tsconfig.json ├── vitest.config.ts ├── web │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ ├── sst-env.d.ts │ │ ├── trpc.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── yarn.lock ├── 2022-08-16-dynamodb-migrations ├── .env ├── .gitignore ├── .vscode │ └── launch.json ├── graphql │ ├── genql │ │ ├── guards.esm.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── schema.graphql │ │ ├── schema.ts │ │ └── types.esm.js │ ├── package.json │ └── schema.graphql ├── package.json ├── services │ ├── core │ │ ├── appointment.ts │ │ ├── article.ts │ │ └── dynamo.ts │ ├── functions │ │ ├── graphql │ │ │ ├── builder.ts │ │ │ ├── graphql.ts │ │ │ ├── schema.ts │ │ │ └── types │ │ │ │ └── article.ts │ │ └── scrap.ts │ ├── package.json │ └── tsconfig.json ├── sst.json ├── stacks │ ├── Api.ts │ ├── Database.ts │ ├── Web.ts │ └── index.ts ├── tsconfig.json ├── vitest.config.ts ├── web │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── assets │ │ │ └── react.svg │ │ ├── index.css │ │ ├── main.tsx │ │ ├── pages │ │ │ └── Article.tsx │ │ ├── sst-env.d.ts │ │ ├── urql.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── yarn.lock └── 2022-08-30-auth ├── .env ├── .gitignore ├── .vscode └── launch.json ├── analyze ├── graphql ├── genql │ ├── guards.esm.js │ ├── index.d.ts │ ├── index.js │ ├── schema.graphql │ ├── schema.ts │ └── types.esm.js ├── package.json └── schema.graphql ├── notes.md ├── package.json ├── services ├── core │ ├── article.ts │ ├── dynamo.ts │ └── user.ts ├── functions │ ├── auth │ │ └── auth.ts │ └── graphql │ │ ├── builder.ts │ │ ├── graphql.ts │ │ ├── schema.ts │ │ └── types │ │ ├── article.ts │ │ └── session.ts ├── package.json ├── test │ └── graphql │ │ └── article.test.ts └── tsconfig.json ├── sst.json ├── stacks ├── Api.ts ├── Database.ts ├── Web.ts └── index.ts ├── tsconfig.json ├── vitest.config.ts ├── web ├── .gitignore ├── index.html ├── package.json ├── public │ └── vite.svg ├── src │ ├── assets │ │ └── react.svg │ ├── index.css │ ├── main.tsx │ ├── pages │ │ └── Article.tsx │ ├── sst-env.d.ts │ ├── urql.ts │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── yarn.lock /2022-08-02-practical-graphql-with-pothos/.env: -------------------------------------------------------------------------------- 1 | # These variables are only available in your SST code. 2 | # To apply them to your Lambda functions, checkout this doc - https://docs.serverless-stack.com/environment-variables#environment-variables-in-lambda-functions 3 | 4 | MY_ENV_VAR=i-am-an-environment-variable 5 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # misc 9 | .DS_Store 10 | 11 | # local env files 12 | .env*.local -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/README.md: -------------------------------------------------------------------------------- 1 | ## Practical GraphQL with Pothos 2 | 3 | [Watch the video](https://youtu.be/RDO_-vE1DRA) 4 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/genql/guards.esm.js: -------------------------------------------------------------------------------- 1 | 2 | var Article_possibleTypes = ['Article'] 3 | export var isArticle = function(obj) { 4 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isArticle"') 5 | return Article_possibleTypes.includes(obj.__typename) 6 | } 7 | 8 | 9 | 10 | var Mutation_possibleTypes = ['Mutation'] 11 | export var isMutation = function(obj) { 12 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isMutation"') 13 | return Mutation_possibleTypes.includes(obj.__typename) 14 | } 15 | 16 | 17 | 18 | var Query_possibleTypes = ['Query'] 19 | export var isQuery = function(obj) { 20 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isQuery"') 21 | return Query_possibleTypes.includes(obj.__typename) 22 | } 23 | 24 | 25 | 26 | var Task_possibleTypes = ['Task'] 27 | export var isTask = function(obj) { 28 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isTask"') 29 | return Task_possibleTypes.includes(obj.__typename) 30 | } 31 | 32 | 33 | 34 | var User_possibleTypes = ['User'] 35 | export var isUser = function(obj) { 36 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isUser"') 37 | return User_possibleTypes.includes(obj.__typename) 38 | } 39 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/genql/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldsSelection, 3 | GraphqlOperation, 4 | ClientOptions, 5 | Observable, 6 | } from '@genql/runtime' 7 | import { SubscriptionClient } from 'subscriptions-transport-ws' 8 | export * from './schema' 9 | import { 10 | QueryRequest, 11 | QueryPromiseChain, 12 | Query, 13 | MutationRequest, 14 | MutationPromiseChain, 15 | Mutation, 16 | } from './schema' 17 | export declare const createClient: (options?: ClientOptions) => Client 18 | export declare const everything: { __scalar: boolean } 19 | export declare const version: string 20 | 21 | export interface Client { 22 | wsClient?: SubscriptionClient 23 | 24 | query( 25 | request: R & { __name?: string }, 26 | ): Promise> 27 | 28 | mutation( 29 | request: R & { __name?: string }, 30 | ): Promise> 31 | 32 | chain: { 33 | query: QueryPromiseChain 34 | 35 | mutation: MutationPromiseChain 36 | } 37 | } 38 | 39 | export type QueryResult = FieldsSelection< 40 | Query, 41 | fields 42 | > 43 | 44 | export declare const generateQueryOp: ( 45 | fields: QueryRequest & { __name?: string }, 46 | ) => GraphqlOperation 47 | export type MutationResult = FieldsSelection< 48 | Mutation, 49 | fields 50 | > 51 | 52 | export declare const generateMutationOp: ( 53 | fields: MutationRequest & { __name?: string }, 54 | ) => GraphqlOperation 55 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/genql/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | linkTypeMap, 3 | createClient as createClientOriginal, 4 | generateGraphqlOperation, 5 | assertSameVersion, 6 | } from '@genql/runtime' 7 | import types from './types.esm' 8 | var typeMap = linkTypeMap(types) 9 | export * from './guards.esm' 10 | 11 | export var version = '2.10.0' 12 | assertSameVersion(version) 13 | 14 | export var createClient = function(options) { 15 | options = options || {} 16 | var optionsCopy = { 17 | url: undefined, 18 | queryRoot: typeMap.Query, 19 | mutationRoot: typeMap.Mutation, 20 | subscriptionRoot: typeMap.Subscription, 21 | } 22 | for (var name in options) { 23 | optionsCopy[name] = options[name] 24 | } 25 | return createClientOriginal(optionsCopy) 26 | } 27 | 28 | export var generateQueryOp = function(fields) { 29 | return generateGraphqlOperation('query', typeMap.Query, fields) 30 | } 31 | export var generateMutationOp = function(fields) { 32 | return generateGraphqlOperation('mutation', typeMap.Mutation, fields) 33 | } 34 | export var generateSubscriptionOp = function(fields) { 35 | return generateGraphqlOperation('subscription', typeMap.Subscription, fields) 36 | } 37 | export var everything = { 38 | __scalar: true, 39 | } 40 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/genql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Article { 2 | id: ID! 3 | title: String! 4 | url: String! 5 | } 6 | 7 | type Mutation { 8 | createArticle(title: String!, url: String!): Article! 9 | } 10 | 11 | type Query { 12 | articles: [Article!]! 13 | tasks: [Task!]! 14 | } 15 | 16 | type Task { 17 | assignee: User 18 | completed: Boolean! 19 | id: ID! 20 | name: String! 21 | } 22 | 23 | type User { 24 | id: ID! 25 | name: String! 26 | taskCount: Int! 27 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/genql/schema.ts: -------------------------------------------------------------------------------- 1 | import {FieldsSelection,Observable} from '@genql/runtime' 2 | 3 | export type Scalars = { 4 | ID: string, 5 | String: string, 6 | Boolean: boolean, 7 | Int: number, 8 | } 9 | 10 | export interface Article { 11 | id: Scalars['ID'] 12 | title: Scalars['String'] 13 | url: Scalars['String'] 14 | __typename: 'Article' 15 | } 16 | 17 | export interface Mutation { 18 | createArticle: Article 19 | __typename: 'Mutation' 20 | } 21 | 22 | export interface Query { 23 | articles: Article[] 24 | tasks: Task[] 25 | __typename: 'Query' 26 | } 27 | 28 | export interface Task { 29 | assignee?: User 30 | completed: Scalars['Boolean'] 31 | id: Scalars['ID'] 32 | name: Scalars['String'] 33 | __typename: 'Task' 34 | } 35 | 36 | export interface User { 37 | id: Scalars['ID'] 38 | name: Scalars['String'] 39 | taskCount: Scalars['Int'] 40 | __typename: 'User' 41 | } 42 | 43 | export interface ArticleRequest{ 44 | id?: boolean | number 45 | title?: boolean | number 46 | url?: boolean | number 47 | __typename?: boolean | number 48 | __scalar?: boolean | number 49 | } 50 | 51 | export interface MutationRequest{ 52 | createArticle?: [{title: Scalars['String'],url: Scalars['String']},ArticleRequest] 53 | __typename?: boolean | number 54 | __scalar?: boolean | number 55 | } 56 | 57 | export interface QueryRequest{ 58 | articles?: ArticleRequest 59 | tasks?: TaskRequest 60 | __typename?: boolean | number 61 | __scalar?: boolean | number 62 | } 63 | 64 | export interface TaskRequest{ 65 | assignee?: UserRequest 66 | completed?: boolean | number 67 | id?: boolean | number 68 | name?: boolean | number 69 | __typename?: boolean | number 70 | __scalar?: boolean | number 71 | } 72 | 73 | export interface UserRequest{ 74 | id?: boolean | number 75 | name?: boolean | number 76 | taskCount?: boolean | number 77 | __typename?: boolean | number 78 | __scalar?: boolean | number 79 | } 80 | 81 | 82 | const Article_possibleTypes: string[] = ['Article'] 83 | export const isArticle = (obj?: { __typename?: any } | null): obj is Article => { 84 | if (!obj?.__typename) throw new Error('__typename is missing in "isArticle"') 85 | return Article_possibleTypes.includes(obj.__typename) 86 | } 87 | 88 | 89 | 90 | const Mutation_possibleTypes: string[] = ['Mutation'] 91 | export const isMutation = (obj?: { __typename?: any } | null): obj is Mutation => { 92 | if (!obj?.__typename) throw new Error('__typename is missing in "isMutation"') 93 | return Mutation_possibleTypes.includes(obj.__typename) 94 | } 95 | 96 | 97 | 98 | const Query_possibleTypes: string[] = ['Query'] 99 | export const isQuery = (obj?: { __typename?: any } | null): obj is Query => { 100 | if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"') 101 | return Query_possibleTypes.includes(obj.__typename) 102 | } 103 | 104 | 105 | 106 | const Task_possibleTypes: string[] = ['Task'] 107 | export const isTask = (obj?: { __typename?: any } | null): obj is Task => { 108 | if (!obj?.__typename) throw new Error('__typename is missing in "isTask"') 109 | return Task_possibleTypes.includes(obj.__typename) 110 | } 111 | 112 | 113 | 114 | const User_possibleTypes: string[] = ['User'] 115 | export const isUser = (obj?: { __typename?: any } | null): obj is User => { 116 | if (!obj?.__typename) throw new Error('__typename is missing in "isUser"') 117 | return User_possibleTypes.includes(obj.__typename) 118 | } 119 | 120 | 121 | export interface ArticlePromiseChain{ 122 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Promise}), 123 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}), 124 | url: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}) 125 | } 126 | 127 | export interface ArticleObservableChain{ 128 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Observable}), 129 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}), 130 | url: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}) 131 | } 132 | 133 | export interface MutationPromiseChain{ 134 | createArticle: ((args: {title: Scalars['String'],url: Scalars['String']}) => ArticlePromiseChain & {get: (request: R, defaultValue?: FieldsSelection) => Promise>}) 135 | } 136 | 137 | export interface MutationObservableChain{ 138 | createArticle: ((args: {title: Scalars['String'],url: Scalars['String']}) => ArticleObservableChain & {get: (request: R, defaultValue?: FieldsSelection) => Observable>}) 139 | } 140 | 141 | export interface QueryPromiseChain{ 142 | articles: ({get: (request: R, defaultValue?: FieldsSelection[]) => Promise[]>}), 143 | tasks: ({get: (request: R, defaultValue?: FieldsSelection[]) => Promise[]>}) 144 | } 145 | 146 | export interface QueryObservableChain{ 147 | articles: ({get: (request: R, defaultValue?: FieldsSelection[]) => Observable[]>}), 148 | tasks: ({get: (request: R, defaultValue?: FieldsSelection[]) => Observable[]>}) 149 | } 150 | 151 | export interface TaskPromiseChain{ 152 | assignee: (UserPromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}), 153 | completed: ({get: (request?: boolean|number, defaultValue?: Scalars['Boolean']) => Promise}), 154 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Promise}), 155 | name: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}) 156 | } 157 | 158 | export interface TaskObservableChain{ 159 | assignee: (UserObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}), 160 | completed: ({get: (request?: boolean|number, defaultValue?: Scalars['Boolean']) => Observable}), 161 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Observable}), 162 | name: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}) 163 | } 164 | 165 | export interface UserPromiseChain{ 166 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Promise}), 167 | name: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}), 168 | taskCount: ({get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Promise}) 169 | } 170 | 171 | export interface UserObservableChain{ 172 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Observable}), 173 | name: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}), 174 | taskCount: ({get: (request?: boolean|number, defaultValue?: Scalars['Int']) => Observable}) 175 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/genql/types.esm.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "scalars": [ 3 | 1, 4 | 2, 5 | 6, 6 | 8 7 | ], 8 | "types": { 9 | "Article": { 10 | "id": [ 11 | 1 12 | ], 13 | "title": [ 14 | 2 15 | ], 16 | "url": [ 17 | 2 18 | ], 19 | "__typename": [ 20 | 2 21 | ] 22 | }, 23 | "ID": {}, 24 | "String": {}, 25 | "Mutation": { 26 | "createArticle": [ 27 | 0, 28 | { 29 | "title": [ 30 | 2, 31 | "String!" 32 | ], 33 | "url": [ 34 | 2, 35 | "String!" 36 | ] 37 | } 38 | ], 39 | "__typename": [ 40 | 2 41 | ] 42 | }, 43 | "Query": { 44 | "articles": [ 45 | 0 46 | ], 47 | "tasks": [ 48 | 5 49 | ], 50 | "__typename": [ 51 | 2 52 | ] 53 | }, 54 | "Task": { 55 | "assignee": [ 56 | 7 57 | ], 58 | "completed": [ 59 | 6 60 | ], 61 | "id": [ 62 | 1 63 | ], 64 | "name": [ 65 | 2 66 | ], 67 | "__typename": [ 68 | 2 69 | ] 70 | }, 71 | "Boolean": {}, 72 | "User": { 73 | "id": [ 74 | 1 75 | ], 76 | "name": [ 77 | 2 78 | ], 79 | "taskCount": [ 80 | 8 81 | ], 82 | "__typename": [ 83 | 2 84 | ] 85 | }, 86 | "Int": {} 87 | } 88 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql/graphql", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@genql/cli": "^2.10.0" 7 | } 8 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Article { 2 | id: ID! 3 | title: String! 4 | url: String! 5 | } 6 | 7 | type Mutation { 8 | createArticle(title: String!, url: String!): Article! 9 | } 10 | 11 | type Query { 12 | articles: [Article!]! 13 | tasks: [Task!]! 14 | } 15 | 16 | type Task { 17 | assignee: User 18 | completed: Boolean! 19 | id: ID! 20 | name: String! 21 | } 22 | 23 | type User { 24 | id: ID! 25 | name: String! 26 | taskCount: Int! 27 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "sst start", 7 | "build": "sst build", 8 | "deploy": "sst deploy", 9 | "remove": "sst remove", 10 | "console": "sst console", 11 | "typecheck": "tsc --noEmit", 12 | "test": "vitest run" 13 | }, 14 | "devDependencies": { 15 | "aws-cdk-lib": "2.32.0", 16 | "@serverless-stack/cli": "^1.6.6", 17 | "@serverless-stack/resources": "^1.6.6", 18 | "typescript": "^4.7.4", 19 | "@tsconfig/node16": "^1.0.3", 20 | "vitest": "^0.20.3" 21 | }, 22 | "workspaces": [ 23 | "services", 24 | "graphql", 25 | "web" 26 | ], 27 | "overrides": { 28 | "graphql": "16.5.0" 29 | } 30 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/core/article.ts: -------------------------------------------------------------------------------- 1 | export * as Article from "./article"; 2 | import { Dynamo } from "./dynamo"; 3 | import { Entity, EntityItem } from "electrodb"; 4 | import { ulid } from "ulid"; 5 | 6 | export const ArticleEntity = new Entity( 7 | { 8 | model: { 9 | version: "1", 10 | entity: "Article", 11 | service: "scratch", 12 | }, 13 | attributes: { 14 | articleID: { 15 | type: "string", 16 | required: true, 17 | readOnly: true, 18 | }, 19 | title: { 20 | type: "string", 21 | required: true, 22 | }, 23 | url: { 24 | type: "string", 25 | required: true, 26 | }, 27 | }, 28 | indexes: { 29 | primary: { 30 | pk: { 31 | field: "pk", 32 | composite: [], 33 | }, 34 | sk: { 35 | field: "sk", 36 | composite: ["articleID"], 37 | }, 38 | }, 39 | }, 40 | }, 41 | Dynamo.Configuration 42 | ); 43 | 44 | export type ArticleEntityType = EntityItem; 45 | 46 | export function create(title: string, url: string) { 47 | return ArticleEntity.create({ 48 | articleID: ulid(), 49 | title, 50 | url, 51 | }).go(); 52 | } 53 | 54 | export async function list() { 55 | return ArticleEntity.query.primary({}).go(); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/core/dynamo.ts: -------------------------------------------------------------------------------- 1 | export * as Dynamo from "./dynamo"; 2 | 3 | import { EntityConfiguration } from "electrodb"; 4 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 5 | 6 | export const Client = new DynamoDBClient({}); 7 | 8 | export const Configuration: EntityConfiguration = { 9 | table: process.env.TABLE_NAME, 10 | client: Client, 11 | }; 12 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/core/task.ts: -------------------------------------------------------------------------------- 1 | export * as Task from "./task" 2 | 3 | export interface Info { 4 | taskID: string; 5 | name: string; 6 | assignee?: string; // Pointing to user 7 | completed: boolean; 8 | } 9 | 10 | const DATABASE = [ 11 | { 12 | taskID: "task:1", 13 | name: "Task 1", 14 | completed: true, 15 | assignee: "user:1" 16 | }, 17 | { 18 | taskID: "task:2", 19 | name: "Task 2", 20 | completed: true, 21 | assignee: "user:1" 22 | }, 23 | { 24 | taskID: "task:1", 25 | name: "Task 1", 26 | completed: false 27 | } 28 | ]; 29 | 30 | export async function list(): Promise { 31 | return DATABASE; 32 | } 33 | 34 | export async function fromID(taskID: string): Promise { 35 | return DATABASE.find(task => task.taskID === taskID); 36 | } 37 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/core/user.ts: -------------------------------------------------------------------------------- 1 | export * as User from "./user" 2 | 3 | import Dataloader from "dataloader" 4 | 5 | export interface Info { 6 | userID: string; 7 | name: string; 8 | } 9 | 10 | const DATABASE: Info[] = [ 11 | { 12 | userID: "user:1", 13 | name: "User 1", 14 | }, 15 | { 16 | userID: "user:2", 17 | name: "User 2", 18 | }, 19 | { 20 | userID: "user:1", 21 | name: "User 3", 22 | } 23 | ]; 24 | 25 | export async function list(): Promise { 26 | // Database call 27 | console.log("User:list") 28 | return DATABASE; 29 | } 30 | 31 | 32 | const userLoader = new Dataloader(async keys => { 33 | console.log("User:fromID") 34 | return keys.map(key => DATABASE.find(user => user.userID === key)); 35 | }) 36 | 37 | export async function fromID(userID: string): Promise { 38 | return userLoader.load(userID) 39 | } 40 | 41 | export async function taskCount(userID: string) { 42 | // Different database call 43 | return Math.floor(Math.random() * 10) 44 | } 45 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/functions/graphql/builder.ts: -------------------------------------------------------------------------------- 1 | import SchemaBuilder from "@pothos/core"; 2 | 3 | export const builder = new SchemaBuilder({}); 4 | 5 | builder.queryType({}); 6 | builder.mutationType({}); 7 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/functions/graphql/graphql.ts: -------------------------------------------------------------------------------- 1 | import { schema } from "./schema"; 2 | import { createGQLHandler } from "@serverless-stack/node/graphql"; 3 | 4 | export const handler = createGQLHandler({ 5 | schema 6 | }); 7 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/functions/graphql/schema.ts: -------------------------------------------------------------------------------- 1 | import { builder } from "./builder"; 2 | 3 | import "./types/article"; 4 | import "./types/project"; 5 | 6 | export const schema = builder.toSchema({}); 7 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/functions/graphql/types/article.ts: -------------------------------------------------------------------------------- 1 | import { Article } from "@graphql/core/article"; 2 | import { builder } from "../builder"; 3 | 4 | const ArticleType = builder 5 | .objectRef("Article") 6 | .implement({ 7 | fields: t => ({ 8 | id: t.exposeID("articleID"), 9 | title: t.exposeString("title"), 10 | url: t.exposeString("url") 11 | }) 12 | }); 13 | 14 | builder.queryFields(t => ({ 15 | articles: t.field({ 16 | type: [ArticleType], 17 | resolve: () => Article.list() 18 | }) 19 | })); 20 | 21 | builder.mutationFields(t => ({ 22 | createArticle: t.field({ 23 | type: ArticleType, 24 | args: { 25 | title: t.arg.string({ required: true }), 26 | url: t.arg.string({ required: true }) 27 | }, 28 | resolve: async (_, args) => Article.create(args.title, args.url) 29 | }) 30 | })); 31 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/functions/graphql/types/project.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@graphql/core/task"; 2 | import { User } from "@graphql/core/user"; 3 | import { builder } from "../builder"; 4 | 5 | export const UserType = builder.objectRef("User").implement({ 6 | fields: t => ({ 7 | id: t.id({ 8 | resolve: async user => user 9 | }), 10 | name: t.string({ 11 | resolve: async user => User.fromID(user).then(user => user!.name) 12 | }), 13 | taskCount: t.int({ 14 | resolve: async user => { 15 | return User.taskCount(user); 16 | } 17 | }) 18 | }) 19 | }); 20 | 21 | export const TaskType = builder.objectRef("Task").implement({ 22 | fields: t => ({ 23 | id: t.exposeID("taskID"), 24 | name: t.exposeString("name"), 25 | completed: t.exposeBoolean("completed"), 26 | assignee: t.field({ 27 | type: UserType, 28 | nullable: true, 29 | resolve: async task => { 30 | if (!task.assignee) return; 31 | return task.assignee; 32 | } 33 | }) 34 | }) 35 | }); 36 | 37 | builder.queryFields(t => ({ 38 | tasks: t.field({ 39 | type: [TaskType], 40 | resolve: async () => await Task.list() 41 | }) 42 | })); 43 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@graphql/services", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": {}, 6 | "dependencies": { 7 | "@aws-sdk/client-dynamodb": "^3.142.0", 8 | "@pothos/core": "^3.13.0", 9 | "@serverless-stack/node": "^1.6.6", 10 | "aws-sdk": "^2.1187.0", 11 | "dataloader": "^2.1.0", 12 | "electrodb": "^1.11.1", 13 | "graphql": "^16.5.0", 14 | "ulid": "^2.3.0" 15 | }, 16 | "devDependencies": { 17 | "@types/aws-lambda": "^8.10.101" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["functions", "core"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "es2017", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@graphql/core/*": ["./core/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/sst.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql", 3 | "region": "us-east-1", 4 | "main": "stacks/index.ts" 5 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/stacks/Api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StackContext, 3 | use, 4 | Api as ApiGateway, 5 | } from "@serverless-stack/resources"; 6 | import { Database } from "./Database"; 7 | 8 | export function Api({ stack }: StackContext) { 9 | const db = use(Database); 10 | 11 | const api = new ApiGateway(stack, "api", { 12 | defaults: { 13 | function: { 14 | permissions: [db], 15 | environment: { 16 | TABLE_NAME: db.tableName, 17 | }, 18 | }, 19 | }, 20 | routes: { 21 | "POST /graphql": { 22 | type: "pothos", 23 | function: { 24 | handler: "functions/graphql/graphql.handler", 25 | }, 26 | schema: "services/functions/graphql/schema.ts", 27 | output: "graphql/schema.graphql", 28 | commands: [ 29 | "npx genql --output ./graphql/genql --schema ./graphql/schema.graphql --esm", 30 | ], 31 | }, 32 | }, 33 | }); 34 | 35 | stack.addOutputs({ 36 | API_URL: api.url, 37 | }); 38 | 39 | return api; 40 | } 41 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/stacks/Database.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, Table } from "@serverless-stack/resources"; 2 | 3 | export function Database({ stack }: StackContext) { 4 | const table = new Table(stack, "table", { 5 | fields: { 6 | pk: "string", 7 | sk: "string", 8 | gsi1pk: "string", 9 | gsi1sk: "string", 10 | }, 11 | primaryIndex: { 12 | partitionKey: "pk", 13 | sortKey: "sk", 14 | }, 15 | globalIndexes: { 16 | gsi1: { 17 | partitionKey: "gsi1pk", 18 | sortKey: "gsi1sk", 19 | }, 20 | }, 21 | }); 22 | 23 | return table; 24 | } 25 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/stacks/Web.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, use, ViteStaticSite } from "@serverless-stack/resources"; 2 | import { Api } from "./Api"; 3 | 4 | export function Web({ stack }: StackContext) { 5 | const api = use(Api); 6 | 7 | const site = new ViteStaticSite(stack, "site", { 8 | path: "web", 9 | buildCommand: "npm run build", 10 | environment: { 11 | VITE_GRAPHQL_URL: api.url + "/graphql", 12 | }, 13 | }); 14 | 15 | stack.addOutputs({ 16 | SITE_URL: site.url, 17 | }); 18 | 19 | return api; 20 | } 21 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/stacks/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "@serverless-stack/resources"; 2 | import { Api } from "./Api"; 3 | import { Database } from "./Database"; 4 | import { Web } from "./Web"; 5 | 6 | export default function main(app: App) { 7 | app.setDefaultFunctionProps({ 8 | runtime: "nodejs16.x", 9 | srcPath: "services", 10 | }); 11 | app.stack(Database).stack(Api).stack(Web); 12 | } 13 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "include": ["stacks"] 4 | } 5 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from "vitest/config"; 4 | 5 | export default defineConfig({ 6 | test: { 7 | testTimeout: 30000, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "sst-env -- vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "^6.3.0", 15 | "urql": "^2.2.3", 16 | "graphql": "^16.5.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.15", 20 | "@types/react-dom": "^18.0.6", 21 | "@vitejs/plugin-react": "^2.0.0", 22 | "typescript": "^4.6.4", 23 | "vite": "^3.0.0", 24 | "@serverless-stack/static-site-env": "^1.6.6" 25 | } 26 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; 5 | import { Provider as UrqlProvider, createClient, defaultExchanges } from "urql"; 6 | import { List } from "./pages/Article"; 7 | 8 | const urql = createClient({ 9 | url: import.meta.env.VITE_GRAPHQL_URL, 10 | exchanges: defaultExchanges 11 | }); 12 | 13 | ReactDOM.createRoot(document.getElementById("root")!).render( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | function App() { 22 | return ( 23 | 24 | 25 | } /> 26 | } /> 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/src/pages/Article.tsx: -------------------------------------------------------------------------------- 1 | import { useTypedMutation, useTypedQuery } from "../urql"; 2 | 3 | interface ArticleForm { 4 | title: string; 5 | url: string; 6 | } 7 | 8 | export function List() { 9 | const [articles] = useTypedQuery({ 10 | query: { 11 | articles: { 12 | id: true, 13 | title: true, 14 | url: true 15 | } 16 | } 17 | }); 18 | 19 | const [, createArticle] = useTypedMutation((opts: ArticleForm) => ({ 20 | createArticle: [ 21 | opts, 22 | { 23 | id: true, 24 | url: true 25 | } 26 | ] 27 | })); 28 | 29 | return ( 30 |
31 |

Articles

32 |

Submit

33 |
{ 35 | e.preventDefault(); 36 | const fd = new FormData(e.currentTarget); 37 | createArticle({ 38 | url: fd.get("url")!.toString(), 39 | title: fd.get("title")!.toString() 40 | }); 41 | e.currentTarget.reset(); 42 | }} 43 | > 44 | 45 | 46 | 47 |
48 |

Latest

49 |
    50 | {articles.data?.articles.map(article => ( 51 |
  1. 52 |
    53 |
    54 | {article.title} - {article.url} 55 |
    56 |
    57 |
  2. 58 | ))} 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/src/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_GRAPHQL_URL: string 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv 9 | } -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/src/urql.ts: -------------------------------------------------------------------------------- 1 | import { OperationContext, RequestPolicy, useQuery, useMutation } from "urql"; 2 | import { useEffect, useState } from "react"; 3 | import { 4 | generateQueryOp, 5 | generateMutationOp, 6 | QueryRequest, 7 | QueryResult, 8 | MutationRequest, 9 | MutationResult 10 | } from "@graphql/graphql/genql"; 11 | 12 | export function useTypedQuery(opts: { 13 | query: Query; 14 | requestPolicy?: RequestPolicy; 15 | context?: Partial; 16 | pause?: boolean; 17 | }) { 18 | const { query, variables } = generateQueryOp(opts.query); 19 | return useQuery>({ 20 | ...opts, 21 | query, 22 | variables 23 | }); 24 | } 25 | 26 | export function useTypedMutation< 27 | Variables extends Record, 28 | Mutation extends MutationRequest 29 | >(builder: (vars: Variables) => Mutation) { 30 | const [mutation, setMutation] = useState(); 31 | const [variables, setVariables] = useState(); 32 | const [result, execute] = useMutation, Variables>( 33 | mutation as any 34 | ); 35 | 36 | function executeWrapper(vars: Variables) { 37 | const mut = builder(vars); 38 | const { query, variables } = generateMutationOp(mut); 39 | setMutation(query); 40 | setVariables(variables); 41 | } 42 | 43 | useEffect(() => { 44 | if (!mutation) return; 45 | execute(variables).then(() => setMutation(undefined)); 46 | }, [mutation]); 47 | 48 | return [result, executeWrapper] as const; 49 | } 50 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /2022-08-02-practical-graphql-with-pothos/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /2022-08-09-trpc/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # misc 9 | .DS_Store 10 | 11 | # local env files 12 | .env*.local -------------------------------------------------------------------------------- /2022-08-09-trpc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trpc", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "sst start", 7 | "build": "sst build", 8 | "deploy": "sst deploy", 9 | "remove": "sst remove", 10 | "console": "sst console", 11 | "typecheck": "tsc --noEmit", 12 | "test": "vitest run" 13 | }, 14 | "devDependencies": { 15 | "aws-cdk-lib": "2.32.0", 16 | "@serverless-stack/cli": "snapshot", 17 | "@serverless-stack/resources": "snapshot", 18 | "typescript": "^4.7.4", 19 | "@tsconfig/node16": "^1.0.3", 20 | "vitest": "^0.21.1" 21 | }, 22 | "workspaces": [ 23 | "services", 24 | "web" 25 | ] 26 | } -------------------------------------------------------------------------------- /2022-08-09-trpc/services/functions/lambda.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandlerV2 } from "aws-lambda"; 2 | 3 | export const handler: APIGatewayProxyHandlerV2 = async (event) => { 4 | return { 5 | statusCode: 200, 6 | headers: { "Content-Type": "text/plain" }, 7 | body: `Hello, World! Your request was received at ${event.requestContext.time}.`, 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /2022-08-09-trpc/services/functions/trpc.ts: -------------------------------------------------------------------------------- 1 | import * as trpc from "@trpc/server"; 2 | import { z } from "zod"; 3 | 4 | const createContext = ({ 5 | event, 6 | context, 7 | }: CreateAWSLambdaContextOptions) => { 8 | return { 9 | user: "foo", 10 | tenant: "blah", 11 | }; 12 | }; // no context 13 | type Context = trpc.inferAsyncReturnType; 14 | 15 | const router = trpc 16 | .router() 17 | .query("hello", { 18 | input: z.string(), 19 | async resolve(req) { 20 | await new Promise((resolve) => setTimeout(resolve, 3000)); 21 | return { 22 | message: `You said ${req.input}`, 23 | }; 24 | }, 25 | }) 26 | .query("bye", { 27 | input: z.object({ 28 | message: z.string(), 29 | }), 30 | async resolve(req) { 31 | return { 32 | message: `Bye ${req.input.message}`, 33 | }; 34 | }, 35 | }) 36 | .mutation("createTodo", { 37 | input: z.object({ 38 | title: z.string(), 39 | }), 40 | async resolve(req) { 41 | await new Promise((resolve) => setTimeout(resolve, 3000)); 42 | return { 43 | id: "1", 44 | title: req.input.title, 45 | }; 46 | }, 47 | }); 48 | 49 | export type Router = typeof router; 50 | 51 | import { 52 | awsLambdaRequestHandler, 53 | CreateAWSLambdaContextOptions, 54 | } from "@trpc/server/adapters/aws-lambda"; 55 | import { APIGatewayProxyEventV2 } from "aws-lambda"; 56 | 57 | export const handler = awsLambdaRequestHandler({ 58 | router, 59 | createContext: createContext, 60 | }); 61 | -------------------------------------------------------------------------------- /2022-08-09-trpc/services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@trpc/services", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": {}, 6 | "dependencies": { 7 | "@trpc/server": "^9.27.0", 8 | "aws-iot-device-sdk": "^2.2.12", 9 | "aws-iot-device-sdk-v2": "^1.8.5", 10 | "aws-sdk": "^2.1190.0", 11 | "zod": "^3.17.10" 12 | }, 13 | "devDependencies": { 14 | "@types/aws-iot-device-sdk": "^2.2.4", 15 | "@types/aws-lambda": "^8.10.101" 16 | } 17 | } -------------------------------------------------------------------------------- /2022-08-09-trpc/services/test/sample.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "vitest"; 2 | 3 | describe("sample", () => { 4 | it("should work", () => { 5 | expect(true).toBe(true); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /2022-08-09-trpc/services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["functions"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "es2017", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "baseUrl": "." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /2022-08-09-trpc/sst.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trpc", 3 | "region": "us-east-1", 4 | "main": "stacks/index.ts" 5 | } -------------------------------------------------------------------------------- /2022-08-09-trpc/stacks/MyStack.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, Api, ViteStaticSite } from "@serverless-stack/resources"; 2 | 3 | export function MyStack({ stack }: StackContext) { 4 | const api = new Api(stack, "api", { 5 | routes: { 6 | "GET /": "functions/lambda.handler", 7 | "GET /trpc/{proxy+}": "functions/trpc.handler", 8 | "POST /trpc/{proxy+}": "functions/trpc.handler", 9 | }, 10 | }); 11 | 12 | const site = new ViteStaticSite(stack, "site", { 13 | path: "./web", 14 | buildCommand: "yarn build", 15 | buildOutput: "dist", 16 | environment: { 17 | VITE_API_URL: api.url, 18 | }, 19 | }); 20 | 21 | stack.addOutputs({ 22 | ApiEndpoint: api.url, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /2022-08-09-trpc/stacks/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "@serverless-stack/resources"; 2 | import { MyStack } from "./MyStack"; 3 | 4 | export default function (app: App) { 5 | app.setDefaultFunctionProps({ 6 | runtime: "nodejs16.x", 7 | srcPath: "services", 8 | bundle: { 9 | format: "esm", 10 | }, 11 | }); 12 | app.stack(MyStack); 13 | } 14 | -------------------------------------------------------------------------------- /2022-08-09-trpc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "include": ["stacks"] 4 | } 5 | -------------------------------------------------------------------------------- /2022-08-09-trpc/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from "vitest/config"; 4 | 5 | export default defineConfig({ 6 | test: { 7 | testTimeout: 30000, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "sst-env -- vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@trpc/client": "^9.27.0", 13 | "@trpc/react": "^9.27.0", 14 | "@trpc/server": "^9.27.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-query": "3" 18 | }, 19 | "devDependencies": { 20 | "@serverless-stack/static-site-env": "snapshot", 21 | "@types/react": "^18.0.15", 22 | "@types/react-dom": "^18.0.6", 23 | "@vitejs/plugin-react": "^2.0.0", 24 | "typescript": "^4.6.4", 25 | "vite": "^3.0.0" 26 | } 27 | } -------------------------------------------------------------------------------- /2022-08-09-trpc/web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import reactLogo from "./assets/react.svg"; 3 | import "./App.css"; 4 | 5 | function App() { 6 | const [count, setCount] = useState(0); 7 | 8 | return ( 9 |
10 | 18 |

Vite + React

19 |
20 | 23 |

24 | Edit src/App.tsx and save to test HMR 25 |

26 |
27 |

28 | Click on the Vite and React logos to learn more 29 |

30 |
31 | ); 32 | } 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { QueryClient, QueryClientProvider } from "react-query"; 4 | import "./index.css"; 5 | import { trpc } from "./trpc"; 6 | 7 | function App() { 8 | const [queryClient] = useState(() => new QueryClient()); 9 | const [trpcClient] = useState(() => { 10 | const token = localStorage.getItem("token"); 11 | return trpc.createClient({ 12 | url: `${import.meta.env.VITE_API_URL}/trpc`, 13 | headers: { 14 | Authorization: `Bearer ${token}`, 15 | }, 16 | }); 17 | }); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | function Sample() { 29 | const hello = trpc.useQuery(["hello", "My message"]); 30 | const createTodo = trpc.useMutation(["createTodo"]); 31 | 32 | return ( 33 |
34 |
{hello.isLoading ? "Loading..." : hello.data?.message}
35 | 45 |
{JSON.stringify(createTodo.data, null, 2)}
46 |
47 | ); 48 | } 49 | 50 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 51 | 52 | 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_API_URL: string 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv 9 | } -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createReactQueryHooks } from "@trpc/react"; 2 | import type { Router } from "../../services/functions/trpc"; 3 | 4 | export const trpc = createReactQueryHooks(); 5 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /2022-08-09-trpc/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/.env: -------------------------------------------------------------------------------- 1 | # These variables are only available in your SST code. 2 | # To apply them to your Lambda functions, checkout this doc - https://docs.serverless-stack.com/environment-variables#environment-variables-in-lambda-functions 3 | 4 | MY_ENV_VAR=i-am-an-environment-variable 5 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # misc 9 | .DS_Store 10 | 11 | # local env files 12 | .env*.local -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/genql/guards.esm.js: -------------------------------------------------------------------------------- 1 | 2 | var Article_possibleTypes = ['Article'] 3 | export var isArticle = function(obj) { 4 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isArticle"') 5 | return Article_possibleTypes.includes(obj.__typename) 6 | } 7 | 8 | 9 | 10 | var Mutation_possibleTypes = ['Mutation'] 11 | export var isMutation = function(obj) { 12 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isMutation"') 13 | return Mutation_possibleTypes.includes(obj.__typename) 14 | } 15 | 16 | 17 | 18 | var Query_possibleTypes = ['Query'] 19 | export var isQuery = function(obj) { 20 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isQuery"') 21 | return Query_possibleTypes.includes(obj.__typename) 22 | } 23 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/genql/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldsSelection, 3 | GraphqlOperation, 4 | ClientOptions, 5 | Observable, 6 | } from '@genql/runtime' 7 | import { SubscriptionClient } from 'subscriptions-transport-ws' 8 | export * from './schema' 9 | import { 10 | QueryRequest, 11 | QueryPromiseChain, 12 | Query, 13 | MutationRequest, 14 | MutationPromiseChain, 15 | Mutation, 16 | } from './schema' 17 | export declare const createClient: (options?: ClientOptions) => Client 18 | export declare const everything: { __scalar: boolean } 19 | export declare const version: string 20 | 21 | export interface Client { 22 | wsClient?: SubscriptionClient 23 | 24 | query( 25 | request: R & { __name?: string }, 26 | ): Promise> 27 | 28 | mutation( 29 | request: R & { __name?: string }, 30 | ): Promise> 31 | 32 | chain: { 33 | query: QueryPromiseChain 34 | 35 | mutation: MutationPromiseChain 36 | } 37 | } 38 | 39 | export type QueryResult = FieldsSelection< 40 | Query, 41 | fields 42 | > 43 | 44 | export declare const generateQueryOp: ( 45 | fields: QueryRequest & { __name?: string }, 46 | ) => GraphqlOperation 47 | export type MutationResult = FieldsSelection< 48 | Mutation, 49 | fields 50 | > 51 | 52 | export declare const generateMutationOp: ( 53 | fields: MutationRequest & { __name?: string }, 54 | ) => GraphqlOperation 55 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/genql/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | linkTypeMap, 3 | createClient as createClientOriginal, 4 | generateGraphqlOperation, 5 | assertSameVersion, 6 | } from '@genql/runtime' 7 | import types from './types.esm' 8 | var typeMap = linkTypeMap(types) 9 | export * from './guards.esm' 10 | 11 | export var version = '2.10.0' 12 | assertSameVersion(version) 13 | 14 | export var createClient = function(options) { 15 | options = options || {} 16 | var optionsCopy = { 17 | url: undefined, 18 | queryRoot: typeMap.Query, 19 | mutationRoot: typeMap.Mutation, 20 | subscriptionRoot: typeMap.Subscription, 21 | } 22 | for (var name in options) { 23 | optionsCopy[name] = options[name] 24 | } 25 | return createClientOriginal(optionsCopy) 26 | } 27 | 28 | export var generateQueryOp = function(fields) { 29 | return generateGraphqlOperation('query', typeMap.Query, fields) 30 | } 31 | export var generateMutationOp = function(fields) { 32 | return generateGraphqlOperation('mutation', typeMap.Mutation, fields) 33 | } 34 | export var generateSubscriptionOp = function(fields) { 35 | return generateGraphqlOperation('subscription', typeMap.Subscription, fields) 36 | } 37 | export var everything = { 38 | __scalar: true, 39 | } 40 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/genql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Article { 2 | id: ID! 3 | title: String! 4 | url: String! 5 | } 6 | 7 | type Mutation { 8 | createArticle(title: String!, url: String!): Article! 9 | } 10 | 11 | type Query { 12 | articles: [Article!]! 13 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/genql/schema.ts: -------------------------------------------------------------------------------- 1 | import {FieldsSelection,Observable} from '@genql/runtime' 2 | 3 | export type Scalars = { 4 | ID: string, 5 | String: string, 6 | Boolean: boolean, 7 | } 8 | 9 | export interface Article { 10 | id: Scalars['ID'] 11 | title: Scalars['String'] 12 | url: Scalars['String'] 13 | __typename: 'Article' 14 | } 15 | 16 | export interface Mutation { 17 | createArticle: Article 18 | __typename: 'Mutation' 19 | } 20 | 21 | export interface Query { 22 | articles: Article[] 23 | __typename: 'Query' 24 | } 25 | 26 | export interface ArticleRequest{ 27 | id?: boolean | number 28 | title?: boolean | number 29 | url?: boolean | number 30 | __typename?: boolean | number 31 | __scalar?: boolean | number 32 | } 33 | 34 | export interface MutationRequest{ 35 | createArticle?: [{title: Scalars['String'],url: Scalars['String']},ArticleRequest] 36 | __typename?: boolean | number 37 | __scalar?: boolean | number 38 | } 39 | 40 | export interface QueryRequest{ 41 | articles?: ArticleRequest 42 | __typename?: boolean | number 43 | __scalar?: boolean | number 44 | } 45 | 46 | 47 | const Article_possibleTypes: string[] = ['Article'] 48 | export const isArticle = (obj?: { __typename?: any } | null): obj is Article => { 49 | if (!obj?.__typename) throw new Error('__typename is missing in "isArticle"') 50 | return Article_possibleTypes.includes(obj.__typename) 51 | } 52 | 53 | 54 | 55 | const Mutation_possibleTypes: string[] = ['Mutation'] 56 | export const isMutation = (obj?: { __typename?: any } | null): obj is Mutation => { 57 | if (!obj?.__typename) throw new Error('__typename is missing in "isMutation"') 58 | return Mutation_possibleTypes.includes(obj.__typename) 59 | } 60 | 61 | 62 | 63 | const Query_possibleTypes: string[] = ['Query'] 64 | export const isQuery = (obj?: { __typename?: any } | null): obj is Query => { 65 | if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"') 66 | return Query_possibleTypes.includes(obj.__typename) 67 | } 68 | 69 | 70 | export interface ArticlePromiseChain{ 71 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Promise}), 72 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}), 73 | url: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}) 74 | } 75 | 76 | export interface ArticleObservableChain{ 77 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Observable}), 78 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}), 79 | url: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}) 80 | } 81 | 82 | export interface MutationPromiseChain{ 83 | createArticle: ((args: {title: Scalars['String'],url: Scalars['String']}) => ArticlePromiseChain & {get: (request: R, defaultValue?: FieldsSelection) => Promise>}) 84 | } 85 | 86 | export interface MutationObservableChain{ 87 | createArticle: ((args: {title: Scalars['String'],url: Scalars['String']}) => ArticleObservableChain & {get: (request: R, defaultValue?: FieldsSelection) => Observable>}) 88 | } 89 | 90 | export interface QueryPromiseChain{ 91 | articles: ({get: (request: R, defaultValue?: FieldsSelection[]) => Promise[]>}) 92 | } 93 | 94 | export interface QueryObservableChain{ 95 | articles: ({get: (request: R, defaultValue?: FieldsSelection[]) => Observable[]>}) 96 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/genql/types.esm.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "scalars": [ 3 | 1, 4 | 2, 5 | 5 6 | ], 7 | "types": { 8 | "Article": { 9 | "id": [ 10 | 1 11 | ], 12 | "title": [ 13 | 2 14 | ], 15 | "url": [ 16 | 2 17 | ], 18 | "__typename": [ 19 | 2 20 | ] 21 | }, 22 | "ID": {}, 23 | "String": {}, 24 | "Mutation": { 25 | "createArticle": [ 26 | 0, 27 | { 28 | "title": [ 29 | 2, 30 | "String!" 31 | ], 32 | "url": [ 33 | 2, 34 | "String!" 35 | ] 36 | } 37 | ], 38 | "__typename": [ 39 | 2 40 | ] 41 | }, 42 | "Query": { 43 | "articles": [ 44 | 0 45 | ], 46 | "__typename": [ 47 | 2 48 | ] 49 | }, 50 | "Boolean": {} 51 | } 52 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@my-sst-app/graphql", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@genql/cli": "^2.10.0" 7 | } 8 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Article { 2 | id: ID! 3 | title: String! 4 | url: String! 5 | } 6 | 7 | type Mutation { 8 | createArticle(title: String!, url: String!): Article! 9 | } 10 | 11 | type Query { 12 | articles: [Article!]! 13 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-sst-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "sst start", 7 | "build": "sst build", 8 | "deploy": "sst deploy", 9 | "remove": "sst remove", 10 | "console": "sst console", 11 | "typecheck": "tsc --noEmit", 12 | "test": "vitest run" 13 | }, 14 | "devDependencies": { 15 | "aws-cdk-lib": "2.32.0", 16 | "@serverless-stack/cli": "^1.7.0", 17 | "@serverless-stack/resources": "^1.7.0", 18 | "typescript": "^4.7.4", 19 | "@tsconfig/node16": "^1.0.3", 20 | "vitest": "^0.22.0" 21 | }, 22 | "workspaces": [ 23 | "services", 24 | "graphql", 25 | "web" 26 | ], 27 | "overrides": { 28 | "graphql": "16.5.0" 29 | } 30 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/core/appointment.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "electrodb"; 2 | import { Dynamo } from "./dynamo"; 3 | 4 | export const AppointmentEntity2 = new Entity( 5 | { 6 | model: { 7 | version: "2", 8 | entity: "Appointment", 9 | service: "scratch" 10 | }, 11 | attributes: { 12 | appointmentID: { 13 | type: "string", 14 | required: true, 15 | readOnly: true 16 | }, 17 | name: { 18 | type: "string", 19 | required: true 20 | }, 21 | cancelled: { 22 | type: ["cancelled", "not-cancelled", "rescheduled"], 23 | default: "not-cancelled" 24 | } 25 | }, 26 | indexes: { 27 | primary: { 28 | pk: { 29 | field: "pk", 30 | composite: ["appointmentID"] 31 | }, 32 | sk: { 33 | field: "sk", 34 | composite: [] 35 | } 36 | }, 37 | list: { 38 | index: "gsi1", 39 | pk: { 40 | field: "gsi1pk", 41 | composite: [] 42 | }, 43 | sk: { 44 | field: "gsi1sk", 45 | composite: ["appointmentID"] 46 | } 47 | } 48 | } 49 | }, 50 | Dynamo.Configuration 51 | ); 52 | 53 | export const AppointmentEntity = new Entity( 54 | { 55 | model: { 56 | version: "3", 57 | entity: "Appointment", 58 | service: "scratch" 59 | }, 60 | attributes: { 61 | appointmentID: { 62 | type: "string", 63 | required: true, 64 | readOnly: true 65 | }, 66 | name: { 67 | type: "string", 68 | required: true 69 | }, 70 | cancelled: { 71 | type: ["cancelled", "not-cancelled", "rescheduled"], 72 | default: "not-cancelled" 73 | }, 74 | shardID: { 75 | type: "string", 76 | required: true 77 | } 78 | }, 79 | indexes: { 80 | primary: { 81 | pk: { 82 | field: "pk", 83 | composite: ["appointmentID"] 84 | }, 85 | sk: { 86 | field: "sk", 87 | composite: [] 88 | } 89 | }, 90 | list: { 91 | index: "gsi1", 92 | pk: { 93 | field: "gsi1pk", 94 | composite: ["shardID"] 95 | }, 96 | sk: { 97 | field: "gsi1sk", 98 | composite: ["appointmentID"] 99 | } 100 | } 101 | } 102 | }, 103 | Dynamo.Configuration 104 | ); 105 | 106 | async function createAppointment() { 107 | //write to 2 108 | //write to 3 109 | } 110 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/core/article.ts: -------------------------------------------------------------------------------- 1 | export * as Article from "./article"; 2 | import { Dynamo } from "./dynamo"; 3 | import { Entity, EntityItem } from "electrodb"; 4 | import { ulid } from "ulid"; 5 | 6 | export const ArticleEntity = new Entity( 7 | { 8 | model: { 9 | version: "1", 10 | entity: "Article", 11 | service: "scratch", 12 | }, 13 | attributes: { 14 | articleID: { 15 | type: "string", 16 | required: true, 17 | readOnly: true, 18 | }, 19 | title: { 20 | type: "string", 21 | required: true, 22 | }, 23 | url: { 24 | type: "string", 25 | required: true, 26 | }, 27 | }, 28 | indexes: { 29 | primary: { 30 | pk: { 31 | field: "pk", 32 | composite: [], 33 | }, 34 | sk: { 35 | field: "sk", 36 | composite: ["articleID"], 37 | }, 38 | }, 39 | }, 40 | }, 41 | Dynamo.Configuration 42 | ); 43 | 44 | export type ArticleEntityType = EntityItem; 45 | 46 | export function create(title: string, url: string) { 47 | return ArticleEntity.create({ 48 | articleID: ulid(), 49 | title, 50 | url, 51 | }).go(); 52 | } 53 | 54 | export async function list() { 55 | return ArticleEntity.query.primary({}).go(); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/core/dynamo.ts: -------------------------------------------------------------------------------- 1 | export * as Dynamo from "./dynamo"; 2 | 3 | import { EntityConfiguration } from "electrodb"; 4 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 5 | 6 | export const Client = new DynamoDBClient({}); 7 | 8 | export const Configuration: EntityConfiguration = { 9 | table: process.env.TABLE_NAME, 10 | client: Client, 11 | }; 12 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/functions/graphql/builder.ts: -------------------------------------------------------------------------------- 1 | import SchemaBuilder from "@pothos/core"; 2 | 3 | export const builder = new SchemaBuilder({}); 4 | 5 | builder.queryType({}); 6 | builder.mutationType({}); 7 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/functions/graphql/graphql.ts: -------------------------------------------------------------------------------- 1 | import { schema } from "./schema"; 2 | import { createGQLHandler } from "@serverless-stack/node/graphql"; 3 | 4 | export const handler = createGQLHandler({ 5 | schema 6 | }); 7 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/functions/graphql/schema.ts: -------------------------------------------------------------------------------- 1 | import { builder } from "./builder"; 2 | 3 | import "./types/article"; 4 | 5 | export const schema = builder.toSchema({}); 6 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/functions/graphql/types/article.ts: -------------------------------------------------------------------------------- 1 | import { Article } from "@my-sst-app/core/article"; 2 | import { builder } from "../builder"; 3 | 4 | const ArticleType = builder 5 | .objectRef("Article") 6 | .implement({ 7 | fields: t => ({ 8 | id: t.exposeID("articleID"), 9 | title: t.exposeString("title"), 10 | url: t.exposeString("url") 11 | }) 12 | }); 13 | 14 | builder.queryFields(t => ({ 15 | articles: t.field({ 16 | type: [ArticleType], 17 | resolve: () => Article.list() 18 | }) 19 | })); 20 | 21 | builder.mutationFields(t => ({ 22 | createArticle: t.field({ 23 | type: ArticleType, 24 | args: { 25 | title: t.arg.string({ required: true }), 26 | url: t.arg.string({ required: true }) 27 | }, 28 | resolve: async (_, args) => Article.create(args.title, args.url) 29 | }) 30 | })); 31 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/functions/scrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AppointmentEntity, 3 | AppointmentEntity2 4 | } from "@my-sst-app/core/appointment"; 5 | import { ulid } from "ulid"; 6 | 7 | async function truncate() { 8 | for (const appt of await AppointmentEntity.scan.go()) { 9 | await AppointmentEntity.delete(appt).go(); 10 | } 11 | } 12 | 13 | export async function handler() { 14 | const tasks = Array(25) 15 | .fill(0) 16 | .map(async (_, index) => { 17 | return AppointmentEntity.query 18 | .list({ 19 | shardID: index.toString() 20 | }) 21 | .go(); 22 | }); 23 | console.log((await Promise.all(tasks)).flat().length); 24 | } 25 | 26 | export function hash(input: string) { 27 | let hash = 0; 28 | if (input.length === 0) return hash; 29 | for (let i = 0; i < input.length; i++) { 30 | hash = input.charCodeAt(i) + ((hash << 5) - hash); 31 | hash = hash & hash; 32 | } 33 | return Math.abs(hash); 34 | } 35 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@my-sst-app/services", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": {}, 6 | "dependencies": { 7 | "aws-sdk": "^2.1196.0", 8 | "@pothos/core": "^3.15.0", 9 | "@serverless-stack/node": "^1.7.0", 10 | "graphql": "^16.6.0", 11 | "ulid": "^2.3.0", 12 | "electrodb": "^1.11.1", 13 | "@aws-sdk/client-dynamodb": "^3.150.0" 14 | }, 15 | "devDependencies": { 16 | "@types/aws-lambda": "^8.10.102" 17 | } 18 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["functions", "core"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "target": "es2017", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@my-sst-app/core/*": ["./core/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/sst.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-sst-app", 3 | "region": "us-east-1", 4 | "main": "stacks/index.ts" 5 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/stacks/Api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StackContext, 3 | use, 4 | Api as ApiGateway, 5 | Function 6 | } from "@serverless-stack/resources"; 7 | import { Database } from "./Database"; 8 | 9 | export function Api({ stack }: StackContext) { 10 | const db = use(Database); 11 | 12 | const api = new ApiGateway(stack, "api", { 13 | defaults: { 14 | function: { 15 | permissions: [db], 16 | environment: { 17 | TABLE_NAME: db.tableName 18 | } 19 | } 20 | }, 21 | routes: { 22 | "POST /graphql": { 23 | type: "pothos", 24 | function: { 25 | handler: "functions/graphql/graphql.handler" 26 | }, 27 | schema: "services/functions/graphql/schema.ts", 28 | output: "graphql/schema.graphql", 29 | commands: [ 30 | "npx genql --output ./graphql/genql --schema ./graphql/schema.graphql --esm" 31 | ] 32 | } 33 | } 34 | }); 35 | 36 | new Function(stack, "scrap", { 37 | handler: "functions/scrap.handler", 38 | permissions: [db], 39 | environment: { 40 | TABLE_NAME: db.tableName 41 | } 42 | }); 43 | 44 | stack.addOutputs({ 45 | API_URL: api.url 46 | }); 47 | 48 | return api; 49 | } 50 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/stacks/Database.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, Table } from "@serverless-stack/resources"; 2 | 3 | export function Database({ stack }: StackContext) { 4 | const table = new Table(stack, "table", { 5 | fields: { 6 | pk: "string", 7 | sk: "string", 8 | gsi1pk: "string", 9 | gsi1sk: "string", 10 | }, 11 | primaryIndex: { 12 | partitionKey: "pk", 13 | sortKey: "sk", 14 | }, 15 | globalIndexes: { 16 | gsi1: { 17 | partitionKey: "gsi1pk", 18 | sortKey: "gsi1sk", 19 | }, 20 | }, 21 | }); 22 | 23 | return table; 24 | } 25 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/stacks/Web.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, use, ViteStaticSite } from "@serverless-stack/resources"; 2 | import { Api } from "./Api"; 3 | 4 | export function Web({ stack }: StackContext) { 5 | const api = use(Api); 6 | 7 | const site = new ViteStaticSite(stack, "site", { 8 | path: "web", 9 | buildCommand: "npm run build", 10 | environment: { 11 | VITE_GRAPHQL_URL: api.url + "/graphql", 12 | }, 13 | }); 14 | 15 | stack.addOutputs({ 16 | SITE_URL: site.url, 17 | }); 18 | 19 | return api; 20 | } 21 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/stacks/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "@serverless-stack/resources"; 2 | import { Api } from "./Api"; 3 | import { Database } from "./Database"; 4 | import { Web } from "./Web"; 5 | 6 | export default function main(app: App) { 7 | app.setDefaultFunctionProps({ 8 | runtime: "nodejs16.x", 9 | srcPath: "services", 10 | }); 11 | app.stack(Database).stack(Api).stack(Web); 12 | } 13 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "include": ["stacks"] 4 | } 5 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from "vitest/config"; 4 | 5 | export default defineConfig({ 6 | test: { 7 | testTimeout: 30000, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "sst-env -- vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "^6.3.0", 15 | "urql": "^2.2.3", 16 | "graphql": "^16.6.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.17", 20 | "@types/react-dom": "^18.0.6", 21 | "@vitejs/plugin-react": "^2.0.1", 22 | "typescript": "^4.6.4", 23 | "vite": "^3.0.7", 24 | "@serverless-stack/static-site-env": "^1.7.0" 25 | } 26 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; 5 | import { Provider as UrqlProvider, createClient, defaultExchanges } from "urql"; 6 | import { List } from "./pages/Article"; 7 | 8 | const urql = createClient({ 9 | url: import.meta.env.VITE_GRAPHQL_URL, 10 | exchanges: defaultExchanges 11 | }); 12 | 13 | ReactDOM.createRoot(document.getElementById("root")!).render( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | function App() { 22 | return ( 23 | 24 | 25 | } /> 26 | } /> 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/src/pages/Article.tsx: -------------------------------------------------------------------------------- 1 | import { useTypedMutation, useTypedQuery } from "../urql"; 2 | 3 | interface ArticleForm { 4 | title: string; 5 | url: string; 6 | } 7 | 8 | export function List() { 9 | const [articles] = useTypedQuery({ 10 | query: { 11 | articles: { 12 | id: true, 13 | title: true, 14 | url: true 15 | } 16 | } 17 | }); 18 | 19 | const [, createArticle] = useTypedMutation((opts: ArticleForm) => ({ 20 | createArticle: [ 21 | opts, 22 | { 23 | id: true, 24 | url: true 25 | } 26 | ] 27 | })); 28 | 29 | return ( 30 |
31 |

Articles

32 |

Submit

33 |
{ 35 | e.preventDefault(); 36 | const fd = new FormData(e.currentTarget); 37 | createArticle({ 38 | url: fd.get("url")!.toString(), 39 | title: fd.get("title")!.toString() 40 | }); 41 | e.currentTarget.reset(); 42 | }} 43 | > 44 | 45 | 46 | 47 |
48 |

Latest

49 |
    50 | {articles.data?.articles.map(article => ( 51 |
  1. 52 |
    53 |
    54 | {article.title} - {article.url} 55 |
    56 |
    57 |
  2. 58 | ))} 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/src/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_GRAPHQL_URL: string 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv 9 | } -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/src/urql.ts: -------------------------------------------------------------------------------- 1 | import { OperationContext, RequestPolicy, useQuery, useMutation } from "urql"; 2 | import { useEffect, useState } from "react"; 3 | import { 4 | generateQueryOp, 5 | generateMutationOp, 6 | QueryRequest, 7 | QueryResult, 8 | MutationRequest, 9 | MutationResult 10 | } from "@my-sst-app/graphql/genql"; 11 | 12 | export function useTypedQuery(opts: { 13 | query: Query; 14 | requestPolicy?: RequestPolicy; 15 | context?: Partial; 16 | pause?: boolean; 17 | }) { 18 | const { query, variables } = generateQueryOp(opts.query); 19 | return useQuery>({ 20 | ...opts, 21 | query, 22 | variables 23 | }); 24 | } 25 | 26 | export function useTypedMutation< 27 | Variables extends Record, 28 | Mutation extends MutationRequest 29 | >(builder: (vars: Variables) => Mutation) { 30 | const [mutation, setMutation] = useState(); 31 | const [variables, setVariables] = useState(); 32 | const [result, execute] = useMutation, Variables>( 33 | mutation as any 34 | ); 35 | 36 | function executeWrapper(vars: Variables) { 37 | const mut = builder(vars); 38 | const { query, variables } = generateMutationOp(mut); 39 | setMutation(query); 40 | setVariables(variables); 41 | } 42 | 43 | useEffect(() => { 44 | if (!mutation) return; 45 | execute(variables).then(() => setMutation(undefined)); 46 | }, [mutation]); 47 | 48 | return [result, executeWrapper] as const; 49 | } 50 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /2022-08-16-dynamodb-migrations/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /2022-08-30-auth/.env: -------------------------------------------------------------------------------- 1 | # These variables are only available in your SST code. 2 | # To apply them to your Lambda functions, checkout this doc - https://docs.serverless-stack.com/environment-variables#environment-variables-in-lambda-functions 3 | 4 | MY_ENV_VAR=i-am-an-environment-variable 5 | -------------------------------------------------------------------------------- /2022-08-30-auth/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # misc 9 | .DS_Store 10 | 11 | # local env files 12 | .env*.local -------------------------------------------------------------------------------- /2022-08-30-auth/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/genql/guards.esm.js: -------------------------------------------------------------------------------- 1 | 2 | var Article_possibleTypes = ['Article'] 3 | export var isArticle = function(obj) { 4 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isArticle"') 5 | return Article_possibleTypes.includes(obj.__typename) 6 | } 7 | 8 | 9 | 10 | var Mutation_possibleTypes = ['Mutation'] 11 | export var isMutation = function(obj) { 12 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isMutation"') 13 | return Mutation_possibleTypes.includes(obj.__typename) 14 | } 15 | 16 | 17 | 18 | var Query_possibleTypes = ['Query'] 19 | export var isQuery = function(obj) { 20 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isQuery"') 21 | return Query_possibleTypes.includes(obj.__typename) 22 | } 23 | 24 | 25 | 26 | var User_possibleTypes = ['User'] 27 | export var isUser = function(obj) { 28 | if (!obj || !obj.__typename) throw new Error('__typename is missing in "isUser"') 29 | return User_possibleTypes.includes(obj.__typename) 30 | } 31 | -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/genql/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FieldsSelection, 3 | GraphqlOperation, 4 | ClientOptions, 5 | Observable, 6 | } from '@genql/runtime' 7 | import { SubscriptionClient } from 'subscriptions-transport-ws' 8 | export * from './schema' 9 | import { 10 | QueryRequest, 11 | QueryPromiseChain, 12 | Query, 13 | MutationRequest, 14 | MutationPromiseChain, 15 | Mutation, 16 | } from './schema' 17 | export declare const createClient: (options?: ClientOptions) => Client 18 | export declare const everything: { __scalar: boolean } 19 | export declare const version: string 20 | 21 | export interface Client { 22 | wsClient?: SubscriptionClient 23 | 24 | query( 25 | request: R & { __name?: string }, 26 | ): Promise> 27 | 28 | mutation( 29 | request: R & { __name?: string }, 30 | ): Promise> 31 | 32 | chain: { 33 | query: QueryPromiseChain 34 | 35 | mutation: MutationPromiseChain 36 | } 37 | } 38 | 39 | export type QueryResult = FieldsSelection< 40 | Query, 41 | fields 42 | > 43 | 44 | export declare const generateQueryOp: ( 45 | fields: QueryRequest & { __name?: string }, 46 | ) => GraphqlOperation 47 | export type MutationResult = FieldsSelection< 48 | Mutation, 49 | fields 50 | > 51 | 52 | export declare const generateMutationOp: ( 53 | fields: MutationRequest & { __name?: string }, 54 | ) => GraphqlOperation 55 | -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/genql/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | linkTypeMap, 3 | createClient as createClientOriginal, 4 | generateGraphqlOperation, 5 | assertSameVersion, 6 | } from '@genql/runtime' 7 | import types from './types.esm' 8 | var typeMap = linkTypeMap(types) 9 | export * from './guards.esm' 10 | 11 | export var version = '2.10.0' 12 | assertSameVersion(version) 13 | 14 | export var createClient = function(options) { 15 | options = options || {} 16 | var optionsCopy = { 17 | url: undefined, 18 | queryRoot: typeMap.Query, 19 | mutationRoot: typeMap.Mutation, 20 | subscriptionRoot: typeMap.Subscription, 21 | } 22 | for (var name in options) { 23 | optionsCopy[name] = options[name] 24 | } 25 | return createClientOriginal(optionsCopy) 26 | } 27 | 28 | export var generateQueryOp = function(fields) { 29 | return generateGraphqlOperation('query', typeMap.Query, fields) 30 | } 31 | export var generateMutationOp = function(fields) { 32 | return generateGraphqlOperation('mutation', typeMap.Mutation, fields) 33 | } 34 | export var generateSubscriptionOp = function(fields) { 35 | return generateGraphqlOperation('subscription', typeMap.Subscription, fields) 36 | } 37 | export var everything = { 38 | __scalar: true, 39 | } 40 | -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/genql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Article { 2 | id: ID! 3 | title: String! 4 | url: String! 5 | } 6 | 7 | type Mutation { 8 | createArticle(title: String!, url: String!): Article! 9 | } 10 | 11 | type Query { 12 | articles: [Article!]! 13 | session: User 14 | } 15 | 16 | type User { 17 | email: String! 18 | id: String! 19 | } -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/genql/schema.ts: -------------------------------------------------------------------------------- 1 | import {FieldsSelection,Observable} from '@genql/runtime' 2 | 3 | export type Scalars = { 4 | ID: string, 5 | String: string, 6 | Boolean: boolean, 7 | } 8 | 9 | export interface Article { 10 | id: Scalars['ID'] 11 | title: Scalars['String'] 12 | url: Scalars['String'] 13 | __typename: 'Article' 14 | } 15 | 16 | export interface Mutation { 17 | createArticle: Article 18 | __typename: 'Mutation' 19 | } 20 | 21 | export interface Query { 22 | articles: Article[] 23 | session?: User 24 | __typename: 'Query' 25 | } 26 | 27 | export interface User { 28 | email: Scalars['String'] 29 | id: Scalars['String'] 30 | __typename: 'User' 31 | } 32 | 33 | export interface ArticleRequest{ 34 | id?: boolean | number 35 | title?: boolean | number 36 | url?: boolean | number 37 | __typename?: boolean | number 38 | __scalar?: boolean | number 39 | } 40 | 41 | export interface MutationRequest{ 42 | createArticle?: [{title: Scalars['String'],url: Scalars['String']},ArticleRequest] 43 | __typename?: boolean | number 44 | __scalar?: boolean | number 45 | } 46 | 47 | export interface QueryRequest{ 48 | articles?: ArticleRequest 49 | session?: UserRequest 50 | __typename?: boolean | number 51 | __scalar?: boolean | number 52 | } 53 | 54 | export interface UserRequest{ 55 | email?: boolean | number 56 | id?: boolean | number 57 | __typename?: boolean | number 58 | __scalar?: boolean | number 59 | } 60 | 61 | 62 | const Article_possibleTypes: string[] = ['Article'] 63 | export const isArticle = (obj?: { __typename?: any } | null): obj is Article => { 64 | if (!obj?.__typename) throw new Error('__typename is missing in "isArticle"') 65 | return Article_possibleTypes.includes(obj.__typename) 66 | } 67 | 68 | 69 | 70 | const Mutation_possibleTypes: string[] = ['Mutation'] 71 | export const isMutation = (obj?: { __typename?: any } | null): obj is Mutation => { 72 | if (!obj?.__typename) throw new Error('__typename is missing in "isMutation"') 73 | return Mutation_possibleTypes.includes(obj.__typename) 74 | } 75 | 76 | 77 | 78 | const Query_possibleTypes: string[] = ['Query'] 79 | export const isQuery = (obj?: { __typename?: any } | null): obj is Query => { 80 | if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"') 81 | return Query_possibleTypes.includes(obj.__typename) 82 | } 83 | 84 | 85 | 86 | const User_possibleTypes: string[] = ['User'] 87 | export const isUser = (obj?: { __typename?: any } | null): obj is User => { 88 | if (!obj?.__typename) throw new Error('__typename is missing in "isUser"') 89 | return User_possibleTypes.includes(obj.__typename) 90 | } 91 | 92 | 93 | export interface ArticlePromiseChain{ 94 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Promise}), 95 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}), 96 | url: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}) 97 | } 98 | 99 | export interface ArticleObservableChain{ 100 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['ID']) => Observable}), 101 | title: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}), 102 | url: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}) 103 | } 104 | 105 | export interface MutationPromiseChain{ 106 | createArticle: ((args: {title: Scalars['String'],url: Scalars['String']}) => ArticlePromiseChain & {get: (request: R, defaultValue?: FieldsSelection) => Promise>}) 107 | } 108 | 109 | export interface MutationObservableChain{ 110 | createArticle: ((args: {title: Scalars['String'],url: Scalars['String']}) => ArticleObservableChain & {get: (request: R, defaultValue?: FieldsSelection) => Observable>}) 111 | } 112 | 113 | export interface QueryPromiseChain{ 114 | articles: ({get: (request: R, defaultValue?: FieldsSelection[]) => Promise[]>}), 115 | session: (UserPromiseChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Promise<(FieldsSelection | undefined)>}) 116 | } 117 | 118 | export interface QueryObservableChain{ 119 | articles: ({get: (request: R, defaultValue?: FieldsSelection[]) => Observable[]>}), 120 | session: (UserObservableChain & {get: (request: R, defaultValue?: (FieldsSelection | undefined)) => Observable<(FieldsSelection | undefined)>}) 121 | } 122 | 123 | export interface UserPromiseChain{ 124 | email: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}), 125 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Promise}) 126 | } 127 | 128 | export interface UserObservableChain{ 129 | email: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}), 130 | id: ({get: (request?: boolean|number, defaultValue?: Scalars['String']) => Observable}) 131 | } -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/genql/types.esm.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "scalars": [ 3 | 1, 4 | 2, 5 | 6 6 | ], 7 | "types": { 8 | "Article": { 9 | "id": [ 10 | 1 11 | ], 12 | "title": [ 13 | 2 14 | ], 15 | "url": [ 16 | 2 17 | ], 18 | "__typename": [ 19 | 2 20 | ] 21 | }, 22 | "ID": {}, 23 | "String": {}, 24 | "Mutation": { 25 | "createArticle": [ 26 | 0, 27 | { 28 | "title": [ 29 | 2, 30 | "String!" 31 | ], 32 | "url": [ 33 | 2, 34 | "String!" 35 | ] 36 | } 37 | ], 38 | "__typename": [ 39 | 2 40 | ] 41 | }, 42 | "Query": { 43 | "articles": [ 44 | 0 45 | ], 46 | "session": [ 47 | 5 48 | ], 49 | "__typename": [ 50 | 2 51 | ] 52 | }, 53 | "User": { 54 | "email": [ 55 | 2 56 | ], 57 | "id": [ 58 | 2 59 | ], 60 | "__typename": [ 61 | 2 62 | ] 63 | }, 64 | "Boolean": {} 65 | } 66 | } -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@auth/graphql", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "dependencies": { 6 | "@genql/cli": "^2.10.0" 7 | } 8 | } -------------------------------------------------------------------------------- /2022-08-30-auth/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Article { 2 | id: ID! 3 | title: String! 4 | url: String! 5 | } 6 | 7 | type Mutation { 8 | createArticle(title: String!, url: String!): Article! 9 | } 10 | 11 | type Query { 12 | articles: [Article!]! 13 | session: User 14 | } 15 | 16 | type User { 17 | email: String! 18 | id: String! 19 | } -------------------------------------------------------------------------------- /2022-08-30-auth/notes.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sst/sst-weekly-repos/9635eaedf4b105480a3d947998477a1bc25be216/2022-08-30-auth/notes.md -------------------------------------------------------------------------------- /2022-08-30-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "sst start", 7 | "build": "sst build", 8 | "deploy": "sst deploy", 9 | "remove": "sst remove", 10 | "console": "sst console", 11 | "typecheck": "tsc --noEmit", 12 | "test": "sst load-config -- vitest run" 13 | }, 14 | "devDependencies": { 15 | "@serverless-stack/cli": "1.10.1", 16 | "@serverless-stack/resources": "1.10.1", 17 | "@tsconfig/node16": "^1.0.3", 18 | "aws-cdk-lib": "2.32.0", 19 | "typescript": "^4.8.2", 20 | "vitest": "^0.22.1" 21 | }, 22 | "workspaces": [ 23 | "services", 24 | "graphql", 25 | "web" 26 | ], 27 | "overrides": { 28 | "graphql": "16.5.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/core/article.ts: -------------------------------------------------------------------------------- 1 | export * as Article from "./article"; 2 | import { Dynamo } from "./dynamo"; 3 | import { Entity, EntityItem } from "electrodb"; 4 | import { ulid } from "ulid"; 5 | 6 | export const ArticleEntity = new Entity( 7 | { 8 | model: { 9 | version: "1", 10 | entity: "Article", 11 | service: "scratch", 12 | }, 13 | attributes: { 14 | articleID: { 15 | type: "string", 16 | required: true, 17 | readOnly: true, 18 | }, 19 | title: { 20 | type: "string", 21 | required: true, 22 | }, 23 | url: { 24 | type: "string", 25 | required: true, 26 | }, 27 | }, 28 | indexes: { 29 | primary: { 30 | pk: { 31 | field: "pk", 32 | composite: [], 33 | }, 34 | sk: { 35 | field: "sk", 36 | composite: ["articleID"], 37 | }, 38 | }, 39 | }, 40 | }, 41 | Dynamo.Configuration 42 | ); 43 | 44 | export type ArticleEntityType = EntityItem; 45 | 46 | export function create(title: string, url: string) { 47 | return ArticleEntity.create({ 48 | articleID: ulid(), 49 | title, 50 | url, 51 | }).go(); 52 | } 53 | 54 | export async function list() { 55 | return ArticleEntity.query.primary({}).go(); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/core/dynamo.ts: -------------------------------------------------------------------------------- 1 | export * as Dynamo from "./dynamo"; 2 | 3 | import { EntityConfiguration } from "electrodb"; 4 | import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; 5 | import { Config } from "@serverless-stack/node/config"; 6 | 7 | export const Client = new DynamoDBClient({}); 8 | 9 | export const Configuration: EntityConfiguration = { 10 | table: Config.TABLE_NAME, 11 | client: Client, 12 | }; 13 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/core/user.ts: -------------------------------------------------------------------------------- 1 | import { Entity, EntityItem } from "electrodb"; 2 | import { ulid } from "ulid"; 3 | import { Dynamo } from "./dynamo"; 4 | 5 | export * as User from "./user" 6 | 7 | export const UserEntity = new Entity({ 8 | model: { 9 | version: "1", 10 | entity: "User", 11 | service: "auth" 12 | }, 13 | attributes: { 14 | userID: { 15 | type: "string", 16 | required: true, 17 | readOnly: true 18 | }, 19 | email: { 20 | type: "string", 21 | required: true 22 | } 23 | }, 24 | indexes: { 25 | primary: { 26 | pk: { 27 | field: "pk", 28 | composite: ["userID"] 29 | }, 30 | sk: { 31 | field: "sk", 32 | composite: [] 33 | } 34 | }, 35 | byEmail: { 36 | index: "gsi1", 37 | pk: { 38 | field: "gsi1pk", 39 | composite: ["email"] 40 | }, 41 | sk: { 42 | field: "gsi1sk", 43 | composite: [] 44 | } 45 | } 46 | } 47 | }, Dynamo.Configuration); 48 | 49 | export type Info = EntityItem; 50 | 51 | export async function fromID(id: string) { 52 | return await UserEntity.get({ 53 | userID: id 54 | }).go(); 55 | } 56 | 57 | export async function fromEmail(email: string) { 58 | const result = await UserEntity.query 59 | .byEmail({ 60 | email: email 61 | }) 62 | .go(); 63 | if (result.length) return result[0]; 64 | } 65 | 66 | export async function create(email: string) { 67 | return UserEntity.create({ 68 | email, 69 | userID: ulid() 70 | }).go() 71 | } 72 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/functions/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@auth/core/user"; 2 | import { 3 | AuthHandler, 4 | GoogleAdapter, 5 | LinkAdapter, 6 | Session 7 | } from "@serverless-stack/node/auth"; 8 | import { Config } from "@serverless-stack/node/config"; 9 | 10 | declare module "@serverless-stack/node/auth" { 11 | export interface SessionTypes { 12 | user: { 13 | userID: string; 14 | }; 15 | } 16 | } 17 | 18 | export const handler = AuthHandler({ 19 | providers: { 20 | google: GoogleAdapter({ 21 | mode: "oidc", 22 | clientID: Config.GOOGLE_CLIENT_ID, 23 | onSuccess: async response => { 24 | let exists = await User.fromEmail(response.claims().email!); 25 | if (!exists) { 26 | exists = await User.create(response.claims().email!); 27 | } 28 | 29 | return Session.parameter({ 30 | redirect: "https://example.com", 31 | type: "user", 32 | properties: { 33 | userID: exists.userID 34 | } 35 | }); 36 | } 37 | }), 38 | link: LinkAdapter({ 39 | onLink: async (link, claims) => { 40 | return { 41 | statusCode: 200, 42 | body: link 43 | }; 44 | }, 45 | onSuccess: async claims => { 46 | let exists = await User.fromEmail(claims.email!); 47 | if (!exists) { 48 | exists = await User.create(claims.email!); 49 | } 50 | 51 | return Session.parameter({ 52 | redirect: "https://example.com", 53 | type: "user", 54 | properties: { 55 | userID: exists.userID 56 | } 57 | }); 58 | }, 59 | onError: async () => { 60 | return { 61 | statusCode: 200, 62 | headers: { 63 | "Content-Type": "application/json" 64 | }, 65 | body: "error" 66 | }; 67 | } 68 | }) 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/functions/graphql/builder.ts: -------------------------------------------------------------------------------- 1 | import SchemaBuilder from "@pothos/core"; 2 | 3 | export const builder = new SchemaBuilder({}); 4 | 5 | builder.queryType({}); 6 | builder.mutationType({}); 7 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/functions/graphql/graphql.ts: -------------------------------------------------------------------------------- 1 | import { schema } from "./schema"; 2 | import { GraphQLHandler } from "@serverless-stack/node/graphql"; 3 | 4 | export const handler = GraphQLHandler({ 5 | schema, 6 | }); 7 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/functions/graphql/schema.ts: -------------------------------------------------------------------------------- 1 | import { builder } from "./builder"; 2 | 3 | import "./types/article"; 4 | import "./types/session"; 5 | 6 | export const schema = builder.toSchema({}); 7 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/functions/graphql/types/article.ts: -------------------------------------------------------------------------------- 1 | import { Article } from "@auth/core/article"; 2 | import { builder } from "../builder"; 3 | 4 | const ArticleType = builder 5 | .objectRef("Article") 6 | .implement({ 7 | fields: t => ({ 8 | id: t.exposeID("articleID"), 9 | title: t.exposeString("title"), 10 | url: t.exposeString("url") 11 | }) 12 | }); 13 | 14 | builder.queryFields(t => ({ 15 | articles: t.field({ 16 | type: [ArticleType], 17 | resolve: () => Article.list() 18 | }) 19 | })); 20 | 21 | builder.mutationFields(t => ({ 22 | createArticle: t.field({ 23 | type: ArticleType, 24 | args: { 25 | title: t.arg.string({ required: true }), 26 | url: t.arg.string({ required: true }) 27 | }, 28 | resolve: async (_, args) => Article.create(args.title, args.url) 29 | }) 30 | })); 31 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/functions/graphql/types/session.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@auth/core/user"; 2 | import { useSession } from "@serverless-stack/node/auth"; 3 | import { builder } from "../builder"; 4 | 5 | const UserType = builder.objectRef("User").implement({ 6 | fields: t => ({ 7 | id: t.exposeString("userID"), 8 | email: t.exposeString("email") 9 | }) 10 | }); 11 | 12 | builder.queryFields(t => ({ 13 | session: t.field({ 14 | type: UserType, 15 | nullable: true, 16 | resolve: () => { 17 | const session = requireUser(); 18 | return User.fromID(session.properties.userID); 19 | } 20 | }) 21 | })); 22 | 23 | function requireUser() { 24 | const session = useSession(); 25 | if (session.type !== "user") { 26 | throw new Error("Expected user session"); 27 | } 28 | return session; 29 | } 30 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@auth/services", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": {}, 6 | "dependencies": { 7 | "@aws-sdk/client-dynamodb": "^3.154.0", 8 | "@pothos/core": "^3.19.0", 9 | "@serverless-stack/node": "1.10.1", 10 | "aws-sdk": "^2.1203.0", 11 | "electrodb": "^1.11.1", 12 | "graphql": "^16.6.0", 13 | "ulid": "^2.3.0" 14 | }, 15 | "devDependencies": { 16 | "@types/aws-lambda": "^8.10.102" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/test/graphql/article.test.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "@serverless-stack/node/config"; 2 | import { expect, it } from "vitest"; 3 | import { createClient } from "@auth/graphql/genql"; 4 | import { Article } from "@auth/core/article"; 5 | 6 | it("create an article", async () => { 7 | const client = createClient({ 8 | url: Config.API_URL + "/graphql", 9 | }); 10 | 11 | const article = await client.mutation({ 12 | createArticle: [ 13 | { title: "Hello world", url: "https://example.com" }, 14 | { 15 | id: true, 16 | }, 17 | ], 18 | }); 19 | const list = await Article.list(); 20 | expect( 21 | list.find((a) => a.articleID === article.createArticle.id) 22 | ).not.toBeNull(); 23 | }); 24 | -------------------------------------------------------------------------------- /2022-08-30-auth/services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["functions", "core", "test"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "baseUrl": ".", 9 | "paths": { 10 | "@auth/core/*": ["./core/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /2022-08-30-auth/sst.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "region": "us-east-1", 4 | "main": "stacks/index.ts" 5 | } -------------------------------------------------------------------------------- /2022-08-30-auth/stacks/Api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | StackContext, 3 | use, 4 | Api as ApiGateway, 5 | Config, 6 | Auth 7 | } from "@serverless-stack/resources"; 8 | import { Database } from "./Database"; 9 | 10 | export function Api({ stack }: StackContext) { 11 | const db = use(Database); 12 | 13 | const auth = new Auth(stack, "auth", { 14 | authenticator: { 15 | config: [new Config.Secret(stack, "GOOGLE_CLIENT_ID")], 16 | handler: "functions/auth/auth.handler" 17 | } 18 | }); 19 | 20 | const api = new ApiGateway(stack, "api", { 21 | defaults: { 22 | function: { 23 | permissions: [db.table], 24 | config: [db.TABLE_NAME] 25 | } 26 | }, 27 | routes: { 28 | "POST /graphql": { 29 | type: "pothos", 30 | function: { 31 | handler: "functions/graphql/graphql.handler", 32 | environment: { 33 | FOO: "test" 34 | } 35 | }, 36 | schema: "services/functions/graphql/schema.ts", 37 | output: "graphql/schema.graphql", 38 | commands: [ 39 | "npx genql --output ./graphql/genql --schema ./graphql/schema.graphql --esm" 40 | ] 41 | } 42 | } 43 | }); 44 | 45 | auth.attach(stack, { api }); 46 | 47 | new Config.Parameter(stack, "API_URL", { 48 | value: api.url 49 | }); 50 | 51 | stack.addOutputs({ 52 | API_URL_OUTPUT: api.url 53 | }); 54 | 55 | return api; 56 | } 57 | -------------------------------------------------------------------------------- /2022-08-30-auth/stacks/Database.ts: -------------------------------------------------------------------------------- 1 | import { Config, StackContext, Table } from "@serverless-stack/resources"; 2 | 3 | export function Database({ stack }: StackContext) { 4 | const table = new Table(stack, "table", { 5 | fields: { 6 | pk: "string", 7 | sk: "string", 8 | gsi1pk: "string", 9 | gsi1sk: "string" 10 | }, 11 | primaryIndex: { 12 | partitionKey: "pk", 13 | sortKey: "sk" 14 | }, 15 | globalIndexes: { 16 | gsi1: { 17 | partitionKey: "gsi1pk", 18 | sortKey: "gsi1sk" 19 | } 20 | } 21 | }); 22 | 23 | return { 24 | table, 25 | TABLE_NAME: new Config.Parameter(stack, "TABLE_NAME", { 26 | value: table.tableName 27 | }) 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /2022-08-30-auth/stacks/Web.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, use, ViteStaticSite } from "@serverless-stack/resources"; 2 | import { Api } from "./Api"; 3 | 4 | export function Web({ stack }: StackContext) { 5 | const api = use(Api); 6 | 7 | const site = new ViteStaticSite(stack, "site", { 8 | path: "web", 9 | buildCommand: "npm run build", 10 | environment: { 11 | VITE_GRAPHQL_URL: api.url + "/graphql", 12 | }, 13 | }); 14 | 15 | stack.addOutputs({ 16 | SITE_URL: site.url, 17 | }); 18 | 19 | return api; 20 | } 21 | -------------------------------------------------------------------------------- /2022-08-30-auth/stacks/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from "@serverless-stack/resources"; 2 | import { Api } from "./Api"; 3 | import { Database } from "./Database"; 4 | import { Web } from "./Web"; 5 | 6 | export default function main(app: App) { 7 | app.setDefaultFunctionProps({ 8 | runtime: "nodejs16.x", 9 | srcPath: "services", 10 | bundle: { 11 | format: "esm" 12 | } 13 | }); 14 | app 15 | .stack(Database) 16 | .stack(Api) 17 | .stack(Web); 18 | } 19 | -------------------------------------------------------------------------------- /2022-08-30-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "include": ["stacks"] 4 | } 5 | -------------------------------------------------------------------------------- /2022-08-30-auth/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from "vitest/config"; 4 | 5 | export default defineConfig({ 6 | test: { 7 | testTimeout: 30000, 8 | }, 9 | logLevel: "info", 10 | esbuild: { 11 | sourcemap: "both", 12 | }, 13 | resolve: { 14 | alias: { 15 | "@auth/core": "./services/core", 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "sst-env -- vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "react-router-dom": "^6.3.0", 15 | "urql": "^3.0.1", 16 | "graphql": "^16.6.0" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^18.0.17", 20 | "@types/react-dom": "^18.0.6", 21 | "@vitejs/plugin-react": "^2.0.1", 22 | "typescript": "^4.6.4", 23 | "vite": "^3.0.7", 24 | "@serverless-stack/static-site-env": "0.0.0-20220827024352" 25 | } 26 | } -------------------------------------------------------------------------------- /2022-08-30-auth/web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; 5 | import { Provider as UrqlProvider, createClient, defaultExchanges } from "urql"; 6 | import { List } from "./pages/Article"; 7 | 8 | const urql = createClient({ 9 | url: import.meta.env.VITE_GRAPHQL_URL, 10 | exchanges: defaultExchanges 11 | }); 12 | 13 | ReactDOM.createRoot(document.getElementById("root")!).render( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | function App() { 22 | return ( 23 | 24 | 25 | } /> 26 | } /> 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/src/pages/Article.tsx: -------------------------------------------------------------------------------- 1 | import { useTypedMutation, useTypedQuery } from "../urql"; 2 | 3 | interface ArticleForm { 4 | title: string; 5 | url: string; 6 | } 7 | 8 | export function List() { 9 | const [articles] = useTypedQuery({ 10 | query: { 11 | articles: { 12 | id: true, 13 | title: true, 14 | url: true 15 | } 16 | } 17 | }); 18 | 19 | const [, createArticle] = useTypedMutation((opts: ArticleForm) => ({ 20 | createArticle: [ 21 | opts, 22 | { 23 | id: true, 24 | url: true 25 | } 26 | ] 27 | })); 28 | 29 | return ( 30 |
31 |

Articles

32 |

Submit

33 |
{ 35 | e.preventDefault(); 36 | const fd = new FormData(e.currentTarget); 37 | createArticle({ 38 | url: fd.get("url")!.toString(), 39 | title: fd.get("title")!.toString() 40 | }); 41 | e.currentTarget.reset(); 42 | }} 43 | > 44 | 45 | 46 | 47 |
48 |

Latest

49 |
    50 | {articles.data?.articles.map(article => ( 51 |
  1. 52 |
    53 |
    54 | {article.title} - {article.url} 55 |
    56 |
    57 |
  2. 58 | ))} 59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/src/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_GRAPHQL_URL: string 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv 9 | } -------------------------------------------------------------------------------- /2022-08-30-auth/web/src/urql.ts: -------------------------------------------------------------------------------- 1 | import { OperationContext, RequestPolicy, useQuery, useMutation } from "urql"; 2 | import { useEffect, useState } from "react"; 3 | import { 4 | generateQueryOp, 5 | generateMutationOp, 6 | QueryRequest, 7 | QueryResult, 8 | MutationRequest, 9 | MutationResult 10 | } from "@auth/graphql/genql"; 11 | 12 | export function useTypedQuery(opts: { 13 | query: Query; 14 | requestPolicy?: RequestPolicy; 15 | context?: Partial; 16 | pause?: boolean; 17 | }) { 18 | const { query, variables } = generateQueryOp(opts.query); 19 | return useQuery>({ 20 | ...opts, 21 | query, 22 | variables 23 | }); 24 | } 25 | 26 | export function useTypedMutation< 27 | Variables extends Record, 28 | Mutation extends MutationRequest 29 | >(builder: (vars: Variables) => Mutation) { 30 | const [mutation, setMutation] = useState(); 31 | const [variables, setVariables] = useState(); 32 | const [result, execute] = useMutation, Variables>( 33 | mutation as any 34 | ); 35 | 36 | function executeWrapper(vars: Variables) { 37 | const mut = builder(vars); 38 | const { query, variables } = generateMutationOp(mut); 39 | setMutation(query); 40 | setVariables(variables); 41 | } 42 | 43 | useEffect(() => { 44 | if (!mutation) return; 45 | execute(variables).then(() => setMutation(undefined)); 46 | }, [mutation]); 47 | 48 | return [result, executeWrapper] as const; 49 | } 50 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /2022-08-30-auth/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | --------------------------------------------------------------------------------