1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-08-09 15:35:22 +02:00

Merge pull request #83 from tmyracle/teller-api-client

Feature: Support Teller Part 1: Teller API client
This commit is contained in:
Josh Pigford 2024-01-15 13:13:20 -06:00 committed by GitHub
commit a777ef0f0f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 715 additions and 16 deletions

View file

@ -17,6 +17,10 @@ NX_PLAID_SECRET=
NX_FINICITY_APP_KEY= NX_FINICITY_APP_KEY=
NX_FINICITY_PARTNER_SECRET= NX_FINICITY_PARTNER_SECRET=
# Teller API keys (https://teller.io)
NX_TELLER_SIGNING_SECRET=
NX_TELLER_APP_ID=
# Email credentials # Email credentials
NX_POSTMARK_FROM_ADDRESS=account@example.com NX_POSTMARK_FROM_ADDRESS=account@example.com
NX_POSTMARK_REPLY_TO_ADDRESS=support@example.com NX_POSTMARK_REPLY_TO_ADDRESS=support@example.com

View file

@ -4,7 +4,6 @@ about: Create a report to help us improve
title: '' title: ''
labels: bug labels: bug
assignees: '' assignees: ''
--- ---
**Describe the bug** **Describe the bug**
@ -12,6 +11,7 @@ A clear and concise description of what the bug is.
**To Reproduce** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@ -24,15 +24,17 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):** **Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari] - OS: [e.g. iOS]
- Version [e.g. 22] - Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):** **Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1] - Device: [e.g. iPhone6]
- Browser [e.g. stock browser, safari] - OS: [e.g. iOS8.1]
- Version [e.g. 22] - Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View file

@ -4,7 +4,6 @@ about: Suggest an idea for this project
title: '' title: ''
labels: '' labels: ''
assignees: '' assignees: ''
--- ---
**Is your feature request related to a problem? Please describe.** **Is your feature request related to a problem? Please describe.**

3
.gitignore vendored
View file

@ -53,4 +53,5 @@ Thumbs.db
migrations.json migrations.json
# Shouldn't happen, but backup since we have a script that generates these locally # Shouldn't happen, but backup since we have a script that generates these locally
*.pem *.pem
certs/

View file

@ -42,6 +42,8 @@ import {
InstitutionProviderFactory, InstitutionProviderFactory,
FinicityWebhookHandler, FinicityWebhookHandler,
PlaidWebhookHandler, PlaidWebhookHandler,
TellerService,
TellerWebhookHandler,
InsightService, InsightService,
SecurityPricingService, SecurityPricingService,
TransactionService, TransactionService,
@ -55,6 +57,7 @@ import { SharedType } from '@maybe-finance/shared'
import prisma from './prisma' import prisma from './prisma'
import plaid, { getPlaidWebhookUrl } from './plaid' import plaid, { getPlaidWebhookUrl } from './plaid'
import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicity' import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicity'
import teller, { getTellerWebhookUrl } from './teller'
import stripe from './stripe' import stripe from './stripe'
import postmark from './postmark' import postmark from './postmark'
import defineAbilityFor from './ability' import defineAbilityFor from './ability'
@ -142,6 +145,14 @@ const finicityService = new FinicityService(
env.NX_FINICITY_ENV === 'sandbox' env.NX_FINICITY_ENV === 'sandbox'
) )
const tellerService = new TellerService(
logger.child({ service: 'TellerService' }),
prisma,
teller,
getTellerWebhookUrl(),
true
)
// account-connection // account-connection
const accountConnectionProviderFactory = new AccountConnectionProviderFactory({ const accountConnectionProviderFactory = new AccountConnectionProviderFactory({
@ -278,6 +289,13 @@ const stripeWebhooks = new StripeWebhookHandler(
stripe stripe
) )
const tellerWebhooks = new TellerWebhookHandler(
logger.child({ service: 'TellerWebhookHandler' }),
prisma,
teller,
accountConnectionService
)
// helper function for parsing JWT and loading User record // helper function for parsing JWT and loading User record
// TODO: update this with roles, identity, and metadata // TODO: update this with roles, identity, and metadata
async function getCurrentUser(jwt: NonNullable<Request['user']>) { async function getCurrentUser(jwt: NonNullable<Request['user']>) {
@ -339,6 +357,8 @@ export async function createContext(req: Request) {
finicityService, finicityService,
finicityWebhooks, finicityWebhooks,
stripeWebhooks, stripeWebhooks,
tellerService,
tellerWebhooks,
insightService, insightService,
marketDataService, marketDataService,
planService, planService,

View file

@ -0,0 +1,11 @@
import { TellerApi } from '@maybe-finance/teller-api'
import { getWebhookUrl } from './webhook'
const teller = new TellerApi()
export default teller
export async function getTellerWebhookUrl() {
const webhookUrl = await getWebhookUrl()
return `${webhookUrl}/v1/teller/webhook`
}

View file

@ -5,5 +5,6 @@ export * from './superjson'
export * from './validate-auth-jwt' export * from './validate-auth-jwt'
export * from './validate-plaid-jwt' export * from './validate-plaid-jwt'
export * from './validate-finicity-signature' export * from './validate-finicity-signature'
export * from './validate-teller-signature'
export { default as maintenance } from './maintenance' export { default as maintenance } from './maintenance'
export * from './identify-user' export * from './identify-user'

View file

@ -0,0 +1,50 @@
import crypto from 'crypto'
import type { RequestHandler } from 'express'
import type { TellerTypes } from '@maybe-finance/teller-api'
import env from '../../env'
// https://teller.io/docs/api/webhooks#verifying-messages
export const validateTellerSignature: RequestHandler = (req, res, next) => {
const signatureHeader = req.headers['teller-signature'] as string | undefined
if (!signatureHeader) {
return res.status(401).send('No Teller-Signature header found')
}
const { timestamp, signatures } = parseTellerSignatureHeader(signatureHeader)
const threeMinutesAgo = Math.floor(Date.now() / 1000) - 3 * 60
if (parseInt(timestamp) < threeMinutesAgo) {
return res.status(408).send('Signature timestamp is too old')
}
const signedMessage = `${timestamp}.${JSON.stringify(req.body as TellerTypes.WebhookData)}`
const expectedSignature = createHmacSha256(signedMessage, env.NX_TELLER_SIGNING_SECRET)
if (!signatures.includes(expectedSignature)) {
return res.status(401).send('Invalid webhook signature')
}
next()
}
const parseTellerSignatureHeader = (
header: string
): { timestamp: string; signatures: string[] } => {
const parts = header.split(',')
const timestampPart = parts.find((p) => p.startsWith('t='))
const signatureParts = parts.filter((p) => p.startsWith('v1='))
if (!timestampPart) {
throw new Error('No timestamp in Teller-Signature header')
}
const timestamp = timestampPart.split('=')[1]
const signatures = signatureParts.map((p) => p.split('=')[1])
return { timestamp, signatures }
}
const createHmacSha256 = (message: string, secret: string): string => {
return crypto.createHmac('sha256', secret).update(message).digest('hex')
}

View file

@ -1,10 +1,11 @@
import { Router } from 'express' import { Router } from 'express'
import { z } from 'zod' import { z } from 'zod'
import type { FinicityTypes } from '@maybe-finance/finicity-api' import type { FinicityTypes } from '@maybe-finance/finicity-api'
import { validatePlaidJwt, validateFinicitySignature } from '../middleware' import { validatePlaidJwt, validateFinicitySignature, validateTellerSignature } from '../middleware'
import endpoint from '../lib/endpoint' import endpoint from '../lib/endpoint'
import stripe from '../lib/stripe' import stripe from '../lib/stripe'
import env from '../../env' import env from '../../env'
import type { TellerTypes } from '@maybe-finance/teller-api'
const router = Router() const router = Router()
@ -131,4 +132,42 @@ router.post(
}) })
) )
router.post(
'/teller/webhook',
process.env.NODE_ENV !== 'development' ? validateTellerSignature : (_req, _res, next) => next(),
endpoint.create({
input: z
.object({
id: z.string(),
payload: z.object({
enrollment_id: z.string(),
reason: z.string(),
}),
timestamp: z.string(),
type: z.string(),
})
.passthrough(),
async resolve({ input, ctx }) {
const { type, id, payload } = input
ctx.logger.info(
`rx[teller_webhook] event eventType=${type} eventId=${id} enrollmentId=${payload.enrollment_id}`
)
// May contain sensitive info, only print at the debug level
ctx.logger.debug(`rx[teller_webhook] event payload`, input)
try {
console.log('handling webhook')
await ctx.tellerWebhooks.handleWebhook(input as TellerTypes.WebhookData)
} catch (err) {
// record error but don't throw
ctx.logger.error(`[teller_webhook] error handling webhook`, err)
}
return { status: 'ok' }
},
})
)
export default router export default router

View file

@ -41,6 +41,9 @@ const envSchema = z.object({
NX_FINICITY_PARTNER_SECRET: z.string(), NX_FINICITY_PARTNER_SECRET: z.string(),
NX_FINICITY_ENV: z.string().default('sandbox'), NX_FINICITY_ENV: z.string().default('sandbox'),
NX_TELLER_SIGNING_SECRET: z.string().default('REPLACE_THIS'),
NX_TELLER_APP_ID: z.string().default('REPLACE_THIS'),
NX_SENTRY_DSN: z.string().optional(), NX_SENTRY_DSN: z.string().optional(),
NX_SENTRY_ENV: z.string().optional(), NX_SENTRY_ENV: z.string().optional(),

View file

@ -15,6 +15,9 @@ const envSchema = z.object({
NX_FINICITY_PARTNER_SECRET: z.string(), NX_FINICITY_PARTNER_SECRET: z.string(),
NX_FINICITY_ENV: z.string().default('sandbox'), NX_FINICITY_ENV: z.string().default('sandbox'),
NX_TELLER_SIGNING_SECRET: z.string().default('REPLACE_THIS'),
NX_TELLER_APP_ID: z.string().default('REPLACE_THIS'),
NX_SENTRY_DSN: z.string().optional(), NX_SENTRY_DSN: z.string().optional(),
NX_SENTRY_ENV: z.string().optional(), NX_SENTRY_ENV: z.string().optional(),

View file

@ -1,4 +1,5 @@
export * from './plaid' export * from './plaid'
export * from './finicity' export * from './finicity'
export * from './teller'
export * from './vehicle' export * from './vehicle'
export * from './property' export * from './property'

View file

@ -0,0 +1,2 @@
export * from './teller.webhook'
export * from './teller.service'

View file

@ -0,0 +1,22 @@
import type { Logger } from 'winston'
import type { AccountConnection, PrismaClient, User } from '@prisma/client'
import type { TellerApi } from '@maybe-finance/teller-api'
export interface ITellerConnect {
generateConnectUrl(userId: User['id'], institutionId: string): Promise<{ link: string }>
generateFixConnectUrl(
userId: User['id'],
accountConnectionId: AccountConnection['id']
): Promise<{ link: string }>
}
export class TellerService {
constructor(
private readonly logger: Logger,
private readonly prisma: PrismaClient,
private readonly teller: TellerApi,
private readonly webhookUrl: string | Promise<string>,
private readonly testMode: boolean
) {}
}

View file

@ -0,0 +1,34 @@
import type { Logger } from 'winston'
import type { PrismaClient } from '@prisma/client'
import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api'
import type { IAccountConnectionService } from '../../account-connection'
export interface ITellerWebhookHandler {
handleWebhook(data: TellerTypes.WebhookData): Promise<void>
}
export class TellerWebhookHandler implements ITellerWebhookHandler {
constructor(
private readonly logger: Logger,
private readonly prisma: PrismaClient,
private readonly teller: TellerApi,
private readonly accountConnectionService: IAccountConnectionService
) {}
/**
* Process Teller webhooks. These handlers should execute as quick as possible and
* long-running operations should be performed in the background.
*/
async handleWebhook(data: TellerTypes.WebhookData) {
switch (data.type) {
case 'webhook.test': {
this.logger.info('Received Teller webhook test')
break
}
default: {
this.logger.warn('Unhandled Teller webhook', { data })
break
}
}
}
}

View file

@ -1,4 +1,4 @@
import CryptoJS from 'crypto-js' import crypto from 'crypto'
export interface ICryptoService { export interface ICryptoService {
encrypt(plainText: string): string encrypt(plainText: string): string
@ -6,13 +6,32 @@ export interface ICryptoService {
} }
export class CryptoService implements ICryptoService { export class CryptoService implements ICryptoService {
constructor(private readonly secret: string) {} private key: Buffer
private ivLength = 16 // Initialization vector length. For AES, this is always 16
constructor(private readonly secret: string) {
// Ensure the key length is suitable for AES-256
this.key = crypto.createHash('sha256').update(String(this.secret)).digest()
}
encrypt(plainText: string) { encrypt(plainText: string) {
return CryptoJS.AES.encrypt(plainText, this.secret).toString() const iv = crypto.randomBytes(this.ivLength)
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv)
let encrypted = cipher.update(plainText, 'utf8', 'hex')
encrypted += cipher.final('hex')
// Include the IV at the start of the encrypted result
return iv.toString('hex') + ':' + encrypted
} }
decrypt(encrypted: string) { decrypt(encrypted: string) {
return CryptoJS.AES.decrypt(encrypted, this.secret).toString(CryptoJS.enc.Utf8) const textParts = encrypted.split(':')
const iv = Buffer.from(textParts.shift()!, 'hex')
const encryptedText = textParts.join(':')
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv)
let decrypted = decipher.update(encryptedText, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
} }
} }

View file

@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View file

@ -0,0 +1,16 @@
/* eslint-disable */
export default {
displayName: 'teller-api',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
testEnvironment: 'node',
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/teller-api',
}

View file

@ -0,0 +1,2 @@
export * from './teller-api'
export * as TellerTypes from './types'

View file

@ -0,0 +1,170 @@
import type { AxiosInstance, AxiosRequestConfig } from 'axios'
import type {
GetAccountResponse,
GetAccountsResponse,
GetAccountBalancesResponse,
GetIdentityResponse,
GetTransactionResponse,
GetTransactionsResponse,
DeleteAccountResponse,
GetAccountDetailsResponse,
GetInstitutionsResponse,
} from './types'
import axios from 'axios'
import * as fs from 'fs'
import * as https from 'https'
/**
* Basic typed mapping for Teller API
*/
export class TellerApi {
private api: AxiosInstance | null = null
/**
* List accounts a user granted access to in Teller Connect
*
* https://teller.io/docs/api/accounts
*/
async getAccounts(): Promise<GetAccountsResponse> {
return this.get<GetAccountsResponse>(`/accounts`)
}
/**
* Get a single account by id
*
* https://teller.io/docs/api/accounts
*/
async getAccount(accountId: string): Promise<GetAccountResponse> {
return this.get<GetAccountResponse>(`/accounts/${accountId}`)
}
/**
* Delete the application's access to an account. Does not delete the account itself.
*
* https://teller.io/docs/api/accounts
*/
async deleteAccount(accountId: string): Promise<DeleteAccountResponse> {
return this.delete<DeleteAccountResponse>(`/accounts/${accountId}`)
}
/**
* Get account details for a single account
*
* https://teller.io/docs/api/account/details
*/
async getAccountDetails(accountId: string): Promise<GetAccountDetailsResponse> {
return this.get<GetAccountDetailsResponse>(`/accounts/${accountId}/details`)
}
/**
* Get account balances for a single account
*
* https://teller.io/docs/api/account/balances
*/
async getAccountBalances(accountId: string): Promise<GetAccountBalancesResponse> {
return this.get<GetAccountBalancesResponse>(`/accounts/${accountId}/balances`)
}
/**
* Get transactions for a single account
*
* https://teller.io/docs/api/transactions
*/
async getTransactions(accountId: string): Promise<GetTransactionsResponse> {
return this.get<GetTransactionsResponse>(`/accounts/${accountId}/transactions`)
}
/**
* Get a single transaction by id
*
* https://teller.io/docs/api/transactions
*/
async getTransaction(
accountId: string,
transactionId: string
): Promise<GetTransactionResponse> {
return this.get<GetTransactionResponse>(
`/accounts/${accountId}/transactions/${transactionId}`
)
}
/**
* Get identity for a single account
*
* https://teller.io/docs/api/identity
*/
async getIdentity(): Promise<GetIdentityResponse> {
return this.get<GetIdentityResponse>(`/identity`)
}
/**
* Get list of supported institutions
*
* https://teller.io/docs/api/identity
*/
async getInstitutions(): Promise<GetInstitutionsResponse> {
return this.get<GetInstitutionsResponse>(`/institutions`)
}
private async getApi(): Promise<AxiosInstance> {
const cert = fs.readFileSync('../../../certs/teller-certificate.pem', 'utf8')
const key = fs.readFileSync('../../../certs/teller-private-key.pem', 'utf8')
const agent = new https.Agent({
cert,
key,
})
if (!this.api) {
this.api = axios.create({
httpsAgent: agent,
baseURL: `https://api.teller.io`,
timeout: 30_000,
headers: {
Accept: 'application/json',
},
})
}
return this.api
}
/** Generic API GET request method */
private async get<TResponse>(
path: string,
params?: any,
config?: AxiosRequestConfig
): Promise<TResponse> {
const api = await this.getApi()
return api.get<TResponse>(path, { params, ...config }).then(({ data }) => data)
}
/** Generic API POST request method */
private async post<TResponse>(
path: string,
body?: any,
config?: AxiosRequestConfig
): Promise<TResponse> {
const api = await this.getApi()
return api.post<TResponse>(path, body, config).then(({ data }) => data)
}
/** Generic API DELETE request method */
private async delete<TResponse>(
path: string,
params?: any,
config?: AxiosRequestConfig
): Promise<TResponse> {
const api = await this.getApi()
return api.delete<TResponse>(path, { params, ...config }).then(({ data }) => data)
}
}

View file

@ -0,0 +1,13 @@
// https://teller.io/docs/api/account/balances
export type AccountBalance = {
account_id: string
ledger: string
available: string
links: {
self: string
account: string
}
}
export type GetAccountBalancesResponse = AccountBalance

View file

@ -0,0 +1,17 @@
// https://teller.io/docs/api/account/details
export type AccountDetails = {
account_id: string
account_number: string
links: {
account: string
self: string
}
routing_numbers: {
ach?: string
wire?: string
bacs?: string
}
}
export type GetAccountDetailsResponse = AccountDetails

View file

@ -0,0 +1,47 @@
// https://teller.io/docs/api/accounts
export type AccountTypes = 'depository' | 'credit'
export type DepositorySubtypes =
| 'checking'
| 'savings'
| 'money market'
| 'certificate of deposit'
| 'treasury'
| 'sweep'
export type CreditSubtype = 'credit_card'
interface BaseAccount {
enrollment_id: string
links: {
balances: string
self: string
transactions: string
}
institution: {
name: string
id: string
}
name: string
currency: string
id: string
last_four: string
status: 'open' | 'closed'
}
interface DepositoryAccount extends BaseAccount {
type: 'depository'
subtype: DepositorySubtypes
}
interface CreditAccount extends BaseAccount {
type: 'credit'
subtype: CreditSubtype
}
export type Account = DepositoryAccount | CreditAccount
export type GetAccountsResponse = { accounts: Account[] }
export type GetAccountResponse = Account
export type DeleteAccountResponse = void

View file

@ -0,0 +1,5 @@
// https://teller.io/docs/api/authentication
export type AuthenticationResponse = {
token: string
}

View file

@ -0,0 +1,39 @@
// https://teller.io/docs/api/identity
import type { Account } from './accounts'
export type Identity = {
type: 'person' | 'business'
names: Name[]
data: string
addresses: Address[]
phone_numbers: PhoneNumber[]
emails: Email[]
}
type Name = {
type: 'name' | 'alias'
}
type Address = {
primary: boolean
street: string
city: string
region: string
postal_code: string
country_code: string
}
type Email = {
data: string
}
type PhoneNumber = {
type: 'mobile' | 'home' | 'work' | 'unknown'
data: string
}
export type GetIdentityResponse = {
account: Account
owners: Identity[]
}[]

View file

@ -0,0 +1,8 @@
export * from './accounts'
export * from './account-balance'
export * from './account-details'
export * from './authentication'
export * from './identity'
export * from './institutions'
export * from './transactions'
export * from './webhooks'

View file

@ -0,0 +1,14 @@
// https://api.teller.io/institutions
// Note: Teller says this is subject to change, specifically the `capabilities` field
export type Institution = {
id: string
name: string
capabilities: Capability[]
}
type Capability = 'detail' | 'balance' | 'transaction' | 'identity'
export type GetInstitutionsResponse = {
institutions: Institution[]
}

View file

@ -0,0 +1,59 @@
// https://teller.io/docs/api/account/transactions
type DetailCategory =
| 'accommodation'
| 'advertising'
| 'bar'
| 'charity'
| 'clothing'
| 'dining'
| 'education'
| 'electronics'
| 'entertainment'
| 'fuel'
| 'general'
| 'groceries'
| 'health'
| 'home'
| 'income'
| 'insurance'
| 'investment'
| 'loan'
| 'office'
| 'phone'
| 'service'
| 'shopping'
| 'software'
| 'sport'
| 'tax'
| 'transport'
| 'transportation'
| 'utilities'
type DetailProcessingStatus = 'pending' | 'complete'
export type Transaction = {
details: {
category?: DetailCategory
processing_status: DetailProcessingStatus
counterparty: {
name?: string
type?: 'organization' | 'person'
}
}
running_balance: string | null
description: string
id: string
date: string
account_id: string
links: {
self: string
account: string
}
amount: string
status: string
type: string
}
export type GetTransactionsResponse = Transaction[]
export type GetTransactionResponse = Transaction

View file

@ -0,0 +1,11 @@
// https://teller.io/docs/api/webhooks
export type WebhookData = {
id: string
payload: {
enrollment_id: string
reason: string
}
timestamp: string
type: string
}

View file

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View file

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"exclude": ["jest.config.ts", "**/*.spec.ts", "s**/*.test.ts"],
"include": ["**/*.ts"]
}

View file

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts"
]
}

View file

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "nx run-many --target serve --projects=client,advisor,server,workers --parallel --host 0.0.0.0 --nx-bail=true --maxParallel=100", "dev": "nx run-many --target serve --projects=client,server,workers --parallel --host 0.0.0.0 --nx-bail=true --maxParallel=100",
"dev:services": "COMPOSE_PROFILES=services docker-compose up -d", "dev:services": "COMPOSE_PROFILES=services docker-compose up -d",
"dev:services:all": "COMPOSE_PROFILES=services,ngrok,stripe docker-compose up", "dev:services:all": "COMPOSE_PROFILES=services,ngrok,stripe docker-compose up",
"dev:workers:test": "nx test workers --skip-nx-cache --runInBand", "dev:workers:test": "nx test workers --skip-nx-cache --runInBand",

View file

@ -23,6 +23,7 @@
"@maybe-finance/server/features": ["libs/server/features/src/index.ts"], "@maybe-finance/server/features": ["libs/server/features/src/index.ts"],
"@maybe-finance/server/shared": ["libs/server/shared/src/index.ts"], "@maybe-finance/server/shared": ["libs/server/shared/src/index.ts"],
"@maybe-finance/shared": ["libs/shared/src/index.ts"], "@maybe-finance/shared": ["libs/shared/src/index.ts"],
"@maybe-finance/teller-api": ["libs/teller-api/src/index.ts"],
"@maybe-finance/trpc": ["apps/server/src/app/trpc.ts"] "@maybe-finance/trpc": ["apps/server/src/app/trpc.ts"]
} }
}, },

View file

@ -390,6 +390,30 @@
}, },
"tags": ["scope:shared"] "tags": ["scope:shared"]
}, },
"teller-api": {
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "libs/teller-api",
"sourceRoot": "libs/teller-api/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/teller-api/**/*.ts"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/teller-api/jest.config.ts",
"passWithNoTests": true
}
}
},
"tags": ["scope:shared"]
},
"workers": { "workers": {
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"root": "apps/workers", "root": "apps/workers",