Skip to content
Open
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
34 changes: 18 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,23 @@
"dependencies": {
"@ai-sdk/anthropic": "^3.0.66",
"@ai-sdk/openai": "^2.0.80",
"@mikro-orm/core": "^6.6.2",
"@mikro-orm/migrations": "^6.6.2",
"@mikro-orm/postgresql": "^6.6.2",
"@open-mercato/ai-assistant": "0.4.8",
"@open-mercato/cache": "0.4.8",
"@open-mercato/cli": "0.4.8",
"@open-mercato/content": "0.4.8",
"@open-mercato/core": "0.4.8",
"@open-mercato/events": "0.4.8",
"@open-mercato/gateway-stripe": "0.4.8",
"@open-mercato/onboarding": "0.4.8",
"@open-mercato/queue": "0.4.8",
"@open-mercato/scheduler": "0.4.8",
"@open-mercato/search": "0.4.8",
"@open-mercato/shared": "0.4.8",
"@open-mercato/ui": "0.4.8",
"@mikro-orm/core": "^7.1.1",
"@mikro-orm/decorators": "^7.1.1",
"@mikro-orm/migrations": "^7.1.1",
"@mikro-orm/postgresql": "^7.1.1",
"@open-mercato/ai-assistant": "0.6.4",
"@open-mercato/cache": "0.6.4",
"@open-mercato/cli": "0.6.4",
"@open-mercato/content": "0.6.4",
"@open-mercato/core": "0.6.4",
"@open-mercato/events": "0.6.4",
"@open-mercato/gateway-stripe": "0.6.4",
"@open-mercato/onboarding": "0.6.4",
"@open-mercato/queue": "0.6.4",
"@open-mercato/scheduler": "0.6.4",
"@open-mercato/search": "0.6.4",
"@open-mercato/shared": "0.6.4",
"@open-mercato/ui": "0.6.4",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.1.8",
Expand Down Expand Up @@ -68,6 +69,7 @@
"react-big-calendar": "^1.19.4",
"react-dom": "19.2.1",
"react-email": "^5.0.6",
"react-is": "^19.2.0",
"remark-gfm": "^4.0.1",
"resend": "^6.5.2",
"semver": "^7.7.3",
Expand Down
17 changes: 16 additions & 1 deletion src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@ registerAppDictionaryLoader(async (locale: Locale): Promise<Record<string, unkno
}
})

// modules.ts inline overrides (replace/disable any contract a module
// presents through the unified modules.ts override surface).
// Importing @open-mercato/ai-assistant here also runs the side-effect
// that registers the AI domain applier with the umbrella dispatcher.
import { enabledModules } from '@/modules'
import { applyModuleOverridesFromEnabledModules } from '@open-mercato/shared/modules/overrides'
import '@open-mercato/ai-assistant'

applyModuleOverridesFromEnabledModules(enabledModules)

// Generated imports (static - works with bundlers)
import { modules } from '@/.mercato/generated/modules.generated'
import { modules } from '@/.mercato/generated/modules.app.generated'
import { entities } from '@/.mercato/generated/entities.generated'
import { diRegistrars } from '@/.mercato/generated/di.generated'
import { E } from '@/.mercato/generated/entities.ids.generated'
Expand All @@ -53,11 +63,16 @@ import { messageTypes } from '@/.mercato/generated/message-types.generated'
import { messageObjectTypes } from '@/.mercato/generated/message-objects.generated'
import { registerMessageTypes } from '@open-mercato/core/modules/messages/lib/message-types-registry'
import { registerMessageObjectTypes } from '@open-mercato/core/modules/messages/lib/message-objects-registry'
import { runBootstrapRegistrations } from '@/.mercato/generated/bootstrap-registrations.generated'
import { allCodeWorkflows } from '@/.mercato/generated/workflows.generated'
import { registerCodeWorkflows } from '@open-mercato/core/modules/workflows/lib/code-registry'

// Register event configs globally (similar to search)
registerEventModuleConfigs(eventModuleConfigs)
registerMessageTypes(messageTypes, { replace: true })
registerMessageObjectTypes(messageObjectTypes, { replace: true })
registerCodeWorkflows(allCodeWorkflows)
runBootstrapRegistrations()

// Bootstrap factory from shared package
import { createBootstrap, isBootstrapped } from '@open-mercato/shared/lib/bootstrap'
Expand Down
36 changes: 36 additions & 0 deletions src/lib/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Raw SQL helpers for cross-module queries.
*
* MikroORM v7 dropped knex, so app code that needs raw cross-module SQL (joining
* tables owned by other modules, where no local entity exists) uses the driver
* connection's `execute()` directly. Placeholders use `?` (MikroORM converts
* them to positional `$n` for Postgres). `rawAll` returns the result rows;
* `rawRun` is for statements without a useful result set (INSERT/UPDATE/DELETE).
*/
import type { EntityManager } from '@mikro-orm/postgresql'

export async function rawAll<T = Record<string, unknown>>(
em: EntityManager,
sql: string,
params: readonly unknown[] = [],
): Promise<T[]> {
const rows = await em.getConnection().execute(sql, params as unknown[], 'all')
return rows as unknown as T[]
}

export async function rawFirst<T = Record<string, unknown>>(
em: EntityManager,
sql: string,
params: readonly unknown[] = [],
): Promise<T | null> {
const rows = await rawAll<T>(em, sql, params)
return rows[0] ?? null
}

export async function rawRun(
em: EntityManager,
sql: string,
params: readonly unknown[] = [],
): Promise<void> {
await em.getConnection().execute(sql, params as unknown[], 'run')
}
4 changes: 2 additions & 2 deletions src/modules/bounties/api/portal/judge/prs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export async function GET(req: Request) {
`SELECT cp.id, cu.first_name, cu.last_name, cp.github_username
FROM competitions_participation cp
JOIN customer_accounts_user cu ON cu.id = cp.customer_user_id
WHERE cp.id = ANY(?)`,
WHERE cp.id IN (?)`,
[participantIds],
)
for (const r of rows) {
Expand All @@ -91,7 +91,7 @@ export async function GET(req: Request) {
const teamMap = new Map<string, string>()
if (teamIds.length > 0) {
const rows = await em.getConnection().execute(
`SELECT id, name FROM teams_team WHERE id = ANY(?)`,
`SELECT id, name FROM teams_team WHERE id IN (?)`,
[teamIds],
)
for (const r of rows) {
Expand Down
4 changes: 2 additions & 2 deletions src/modules/bounties/data/enrichers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const enrichers: ResponseEnricher[] = [
`SELECT cp.id, cu.first_name, cu.last_name, cp.github_username
FROM competitions_participation cp
JOIN customer_accounts_user cu ON cu.id = cp.customer_user_id
WHERE cp.id = ANY(?)`,
WHERE cp.id IN (?)`,
[participantIds]
)
for (const r of rows) {
Expand All @@ -73,7 +73,7 @@ export const enrichers: ResponseEnricher[] = [
const teamMap = new Map<string, string>()
if (teamIds.length > 0) {
const rows = await em.getConnection().execute(
`SELECT id, name FROM teams_team WHERE id = ANY(?)`,
`SELECT id, name FROM teams_team WHERE id IN (?)`,
[teamIds]
)
for (const r of rows) {
Expand Down
2 changes: 1 addition & 1 deletion src/modules/bounties/data/entities.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Entity, PrimaryKey, Property, Unique, Index } from '@mikro-orm/core'
import { Entity, PrimaryKey, Property, Unique, Index } from '@mikro-orm/decorators/legacy'

// ── Enums (as const objects) ──────────────��─────────────────────────

Expand Down
40 changes: 17 additions & 23 deletions src/modules/competitions/api/admin/invitations/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { rawAll } from '../../../../../lib/db'
import { NextResponse } from 'next/server'
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
Expand All @@ -20,43 +21,36 @@ export async function GET(req: Request) {

const container = await createRequestContainer()
const em = container.resolve('em') as EntityManager
const knex = (em as any).getConnection().getKnex()

// Resolve organization scope from the request (cookie-selected org)
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
const organizationIds = scope.filterIds

let query = knex('competitions_invitation as ci')
.join('customer_user_invitations as cui', 'cui.id', 'ci.customer_invitation_id')
.where('ci.tenant_id', auth.tenantId)
.select(
'ci.id',
'ci.customer_invitation_id',
'ci.competition_id',
'ci.participation_role',
'ci.created_at',
'cui.email',
'cui.display_name',
'cui.accepted_at',
'cui.cancelled_at',
'cui.expires_at',
)
.orderBy('ci.created_at', 'desc')
.limit(200)

const conds: string[] = ['ci.tenant_id = ?']
const qParams: unknown[] = [auth.tenantId]
if (competitionId) {
query = query.where('ci.competition_id', competitionId)
conds.push('ci.competition_id = ?')
qParams.push(competitionId)
}
if (organizationIds && organizationIds.length > 0) {
query = query.whereIn('ci.organization_id', organizationIds)
conds.push('ci.organization_id IN (?)')
qParams.push(organizationIds)
}

const rows = await query
const rows = await rawAll<any>(em, `
SELECT ci.id, ci.customer_invitation_id, ci.competition_id, ci.participation_role, ci.created_at,
cui.email, cui.display_name, cui.accepted_at, cui.cancelled_at, cui.expires_at
FROM competitions_invitation ci
JOIN customer_user_invitations cui ON cui.id = ci.customer_invitation_id
WHERE ${conds.join(' AND ')}
ORDER BY ci.created_at DESC
LIMIT 200
`, qParams)

// Also fetch competition names
const compIds = [...new Set(rows.map((r: any) => r.competition_id))]
const compRows = compIds.length > 0
? await knex('competitions_competition').select('id', 'name').whereIn('id', compIds)
? await rawAll<{ id: string; name: string }>(em, `SELECT id, name FROM competitions_competition WHERE id IN (?)`, [compIds])
: []
const compMap = new Map<string, string>(compRows.map((r: any) => [r.id, r.name]))

Expand Down
51 changes: 17 additions & 34 deletions src/modules/competitions/api/admin/participant-invitations/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { rawAll, rawFirst } from '../../../../../lib/db'
import { NextResponse } from 'next/server'
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
Expand All @@ -23,44 +24,26 @@ export async function GET(req: Request) {

const container = await createRequestContainer()
const em = container.resolve('em') as EntityManager
const knex = (em as any).getConnection().getKnex()

// Find competition invitations for this user+competition, joined with framework invitation data
const rows = await knex('competitions_invitation as ci')
.join('customer_user_invitations as cui', 'cui.id', 'ci.customer_invitation_id')
.where('ci.competition_id', competitionId)
.where('ci.tenant_id', auth.tenantId)
.where('cui.email_hash', knex.raw(
`(SELECT email_hash FROM customer_users WHERE id = ? AND tenant_id = ? LIMIT 1)`,
[customerUserId, auth.tenantId],
))
.orWhere(function (this: any) {
// Also match by direct email lookup from customer_user_invitations joined to competitions_invitation
this.where('ci.competition_id', competitionId)
.where('ci.tenant_id', auth.tenantId)
})
.select(
'ci.id',
'ci.customer_invitation_id',
'ci.competition_id',
'ci.participation_role',
'ci.created_at',
'cui.email',
'cui.display_name',
'cui.accepted_at',
'cui.cancelled_at',
'cui.expires_at',
)
.orderBy('ci.created_at', 'desc')
.limit(20)
// Find competition invitations for this competition (results are filtered by the
// customer user's email below, mirroring the previous query's effective behavior).
const rows = await rawAll<any>(em, `
SELECT ci.id, ci.customer_invitation_id, ci.competition_id, ci.participation_role, ci.created_at,
cui.email, cui.display_name, cui.accepted_at, cui.cancelled_at, cui.expires_at
FROM competitions_invitation ci
JOIN customer_user_invitations cui ON cui.id = ci.customer_invitation_id
WHERE ci.competition_id = ? AND ci.tenant_id = ?
ORDER BY ci.created_at DESC
LIMIT 20
`, [competitionId, auth.tenantId])

// Filter to only invitations whose email matches the customer user's email
// First get the user's email
const userRow = await knex('customer_users')
.where('id', customerUserId)
.where('tenant_id', auth.tenantId)
.select('email')
.first()
const userRow = await rawFirst<{ email: string | null }>(
em,
`SELECT email FROM customer_users WHERE id = ? AND tenant_id = ? LIMIT 1`,
[customerUserId, auth.tenantId],
)

const userEmail = userRow?.email?.toLowerCase()

Expand Down
12 changes: 4 additions & 8 deletions src/modules/competitions/api/checkin/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { rawFirst } from '../../../../lib/db'
import { NextResponse } from 'next/server'
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
Expand Down Expand Up @@ -30,7 +31,6 @@ export async function POST(req: Request) {
const body = await req.json()
const container = await createRequestContainer()
const em = container.resolve('em') as EntityManager
const knex = (em as any).getConnection().getKnex()

let participation: CompetitionParticipation | null = null

Expand All @@ -42,11 +42,7 @@ export async function POST(req: Request) {
id: byId.data.participation_id, tenantId: auth.tenantId, deletedAt: null,
} as FilterQuery<CompetitionParticipation>)
} else if (byEmail.success && byEmail.data.email) {
const userRow = await knex('customer_users')
.select('id')
.where('email', byEmail.data.email)
.where('tenant_id', auth.tenantId)
.first()
const userRow = await rawFirst<{ id: string }>(em, `SELECT id FROM customer_users WHERE email = ? AND tenant_id = ? LIMIT 1`, [byEmail.data.email, auth.tenantId])
if (userRow) {
participation = await em.findOne(CompetitionParticipation, {
customerUserId: userRow.id,
Expand All @@ -61,7 +57,7 @@ export async function POST(req: Request) {

if (!participation) return NextResponse.json({ error: 'Participation not found' }, { status: 404 })
if (participation.checkedIn) {
const alreadyRow = await knex('customer_users').select('display_name', 'email').where('id', participation.customerUserId).first()
const alreadyRow = await rawFirst<{ display_name: string | null; email: string | null }>(em, `SELECT display_name, email FROM customer_users WHERE id = ? LIMIT 1`, [participation.customerUserId])
return NextResponse.json({ ok: true, already: true, displayName: alreadyRow?.display_name ?? null, email: alreadyRow?.email ?? null })
}

Expand All @@ -70,7 +66,7 @@ export async function POST(req: Request) {
await em.persistAndFlush(participation)

// Resolve display name for response
const displayRow = await knex('customer_users').select('display_name', 'email').where('id', participation.customerUserId).first()
const displayRow = await rawFirst<{ display_name: string | null; email: string | null }>(em, `SELECT display_name, email FROM customer_users WHERE id = ? LIMIT 1`, [participation.customerUserId])

return NextResponse.json({
ok: true,
Expand Down
Loading