Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing internal actions and a command to run them #5

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@interval/server",
"description": "Interval Server is the central server for Interval apps",
"version": "1.0.1",
"version": "1.1.0-dev.0+internal",
"license": "MIT",
"engines": {
"node": ">=16"
Expand All @@ -12,6 +12,7 @@
"bin": {
"interval-server": "dist/src/entry.js"
},
"files": ["dist/"],
"scripts": {
"lint": "eslint src",
"seed": "prisma db seed",
Expand All @@ -22,7 +23,7 @@
"build": "rm -rf dist && prisma generate && yarn server:build && yarn wss:build && yarn client:build",
"pkg": "./scripts/pkg.sh",
"pub": "./scripts/pub.sh",
"start": "NODE_ENV=production ./dist/src/entry.js",
"start": "NODE_ENV=production node ./dist/src/entry.js",
"docker": "./scripts/docker-build.sh",
"client:build": "vite build",
"client:clean": "rm -r node_modules/.vite",
Expand Down
71 changes: 48 additions & 23 deletions src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,16 @@ program
.name('interval-server')
.description('Interval Server is the central server for Interval apps')
.option('-v, --verbose', 'verbose output')
.addCommand(new Command('start').description('starts Interval Server'))
.addCommand(
new Command('start')
.description('starts Interval Server')
.addOption(
new Option(
'--internal-actions',
'start interval internal actions along with the server'
)
)
)
.addCommand(
new Command('db-init').addOption(
new Option(
Expand All @@ -177,34 +186,50 @@ program

const [cmd, ...args] = program.parse().args
async function main() {
if (cmd === 'start') {
const envVars = (await import('./env')).default
// start the internal web socket server
import('./wss/index')
switch (cmd) {
case 'start': {
const envVars = (await import('./env')).default
// start the internal web socket server
import('./wss/index')

const app = express()
const app = express()

const mainAppServer = (await import('./server/index')).default
const mainAppServer = (await import('./server/index')).default

app.use(mainAppServer)
app.use(mainAppServer)

const server = http.createServer(app)
const server = http.createServer(app)

const wss = new WebSocketServer({ server, path: '/websocket' })
const { setupWebSocketServer } = await import('./wss/wss')
setupWebSocketServer(wss)
const wss = new WebSocketServer({ server, path: '/websocket' })
const { setupWebSocketServer } = await import('./wss/wss')
setupWebSocketServer(wss)

server.listen(Number(envVars.PORT), () => {
logger.info(
`📡 Interval Server listening at http://localhost:${envVars.PORT}`
)
})
} else if (cmd === 'db-init') {
logger.info('Initializing a database...')
initDb({ skipCreate: args.includes('--skip-create') }).catch(() => {
logger.error(`Failed to initialize database.`)
process.exit(1)
})
server.listen(Number(envVars.PORT), () => {
logger.info(
`📡 Interval Server listening at http://localhost:${envVars.PORT}`
)

if (args.includes('--internal-actions')) {
import('./interval').catch(err => {
logger.error('Failed starting internal actions:', err)
})
}
})

break
}
case 'internal': {
await import('./interval')
break
}
case 'db-init': {
logger.info('Initializing a database...')
initDb({ skipCreate: args.includes('--skip-create') }).catch(() => {
logger.error('Failed to initialize database.')
process.exit(1)
})
break
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const schema = z.object({
WSS_API_SECRET: z.string(),
AUTH_COOKIE_SECRET: z.string(),

// for internal interval host
INTERVAL_KEY: z.string().optional(),

GIT_COMMIT: z.string().optional(),
PORT: z.string().optional().default('3000'),

Expand Down
21 changes: 21 additions & 0 deletions src/interval/helpers/findUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import prisma from '~/server/prisma'
export default async function findUsers(query: string) {
return prisma.user.findMany({
where: {
OR: [
{
firstName: {
mode: 'insensitive',
contains: query,
},
},
{
lastName: {
mode: 'insensitive',
contains: query,
},
},
],
},
})
}
45 changes: 45 additions & 0 deletions src/interval/helpers/orgRowMenuItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MenuItem } from '@interval/sdk/dist/types'
import { Organization, OrganizationSSO } from '@prisma/client'

export default function orgRowMenuItems(
row: Organization & { sso?: Pick<OrganizationSSO, 'id'> | null }
): MenuItem[] {
return [
{
label: 'Browse app structure',
action: 'organizations/app_structure',
params: { org: row.slug },
},
{
label: 'Change slug',
action: 'organizations/change_slug',
params: { org: row.slug },
},
{
label: 'Enable SSO',
action: 'organizations/create_org_sso',
params: { org: row.slug },
},
{
label: 'Disable SSO',
action: 'organizations/disable_org_sso',
params: { org: row.slug },
disabled: row.sso === null,
},
{
label: 'Toggle feature flag',
action: 'organizations/org_feature_flag',
params: { org: row.slug },
},
{
label: 'Transfer owner',
action: 'organizations/transfer_ownership',
params: { org: row.slug },
},
{
label: 'Create transaction history export',
action: 'organizations/create_transaction_history_export',
params: { org: row.slug },
},
]
}
8 changes: 8 additions & 0 deletions src/interval/helpers/renderUserResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { User } from '@prisma/client'

export default function renderUserResult(user: User) {
return {
label: [user.firstName, user.lastName].join(' '),
description: user.email,
}
}
21 changes: 21 additions & 0 deletions src/interval/helpers/requireOrg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ctx } from '@interval/sdk'
import prisma from '~/server/prisma'
import selectOrganization from './selectOrganization'

export default async function requireOrg(paramName = 'org') {
if (paramName in ctx.params) {
const org = await prisma.organization.findFirst({
where: { slug: String(ctx.params[paramName]) },
include: {
sso: true,
environments: true,
},
})

if (!org) throw new Error('Org not found')

return org
}

return selectOrganization()
}
9 changes: 9 additions & 0 deletions src/interval/helpers/requireParam.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ctx } from '@interval/sdk'

export default function requireParam(key: string) {
if (ctx.params[key] === undefined) {
throw new Error(`Missing required param: ${key}`)
}

return String(ctx.params[key])
}
40 changes: 40 additions & 0 deletions src/interval/helpers/searchForOrganization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { io } from '@interval/sdk'
import { Prisma } from '@prisma/client'

import prisma from '~/server/prisma'

export default async function searchForOrganization() {
return io.search<Prisma.OrganizationGetPayload<{ include: { owner: true } }>>(
'Select organization',
{
async onSearch(query) {
return prisma.organization.findMany({
where: {
OR: [
{
name: {
search: query,
mode: 'insensitive',
},
},
{
name: {
contains: query,
mode: 'insensitive',
},
},
],
},
include: {
owner: true,
},
})
},
renderResult: org => ({
value: org.id,
label: org.name,
description: `Owner: ${org.owner.firstName} ${org.owner.lastName} (${org.owner.email}), Slug: ${org.slug}`,
}),
}
)
}
22 changes: 22 additions & 0 deletions src/interval/helpers/selectOrganization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import prisma from '~/server/prisma'
import searchForOrganization from './searchForOrganization'

export default async function selectOrganization() {
const selected = await searchForOrganization()

const org = await prisma.organization.findUnique({
where: {
id: selected.id,
},
include: {
sso: true,
environments: true,
},
})

if (!org) {
throw new Error("Organization doesn't exist?")
}

return org
}
10 changes: 10 additions & 0 deletions src/interval/helpers/selectUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { io } from '@interval/sdk'
import findUsers from './findUsers'
import renderUserResult from './renderUserResult'

export default function selectUser() {
return io.search('Select a user', {
onSearch: async q => await findUsers(q),
renderResult: renderUserResult,
})
}
9 changes: 9 additions & 0 deletions src/interval/helpers/staticQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Don't use this with any query strings that aren't static

import { io } from '@interval/sdk'
import prisma from '~/server/prisma'

export default async function staticQuery(query: string) {
const rows: Record<string, string>[] = await prisma.$queryRawUnsafe(query)
await io.display.table('Rows', { data: rows })
}
17 changes: 17 additions & 0 deletions src/interval/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import path from 'path'
import { Interval } from '@interval/sdk'
import env from '../env'

if (!env.INTERVAL_KEY) {
throw new Error(
'Environment variable INTERVAL_KEY required for internal actions.'
)
}

const interval = new Interval({
endpoint: `${env.APP_URL}/websocket`,
apiKey: env.INTERVAL_KEY,
routesDirectory: path.resolve(__dirname, 'routes'),
})

interval.listen()
Loading