feat(platform-service): add memory-items API backed by Cosmos
This commit is contained in:
parent
cb728d3dfe
commit
17c41e8441
@ -85,7 +85,8 @@ Voice capture pipeline
|
|||||||
|
|
||||||
Text capture
|
Text capture
|
||||||
|
|
||||||
- [ ] **P0** — Persist raw text to `memory_items` Cosmos container (currently in-memory)
|
- [x] **P0** — Backend: persist raw text to `memory_items` Cosmos container (`platform-service` `POST /api/memory-items`)
|
||||||
|
- [ ] **P0** — Mobile: call `platform-service` `POST /api/memory-items` during capture/triage flow
|
||||||
|
|
||||||
Image/screenshot capture
|
Image/screenshot capture
|
||||||
|
|
||||||
@ -94,7 +95,8 @@ Image/screenshot capture
|
|||||||
|
|
||||||
Triage persistence
|
Triage persistence
|
||||||
|
|
||||||
- [ ] **P0** — Store structured `MemoryItem` in Cosmos DB with all triage fields (currently in-memory)
|
- [x] **P0** — Backend: store structured `MemoryItem` in Cosmos DB with triage fields (`platform-service` `memory_items`)
|
||||||
|
- [ ] **P0** — Mobile: wire triage confirmation to send `triageResult` + `brainIds` to backend (`POST /api/memory-items`)
|
||||||
|
|
||||||
Widgets
|
Widgets
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,11 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
|||||||
notification_prefs: { partitionKeyPath: '/userId' },
|
notification_prefs: { partitionKeyPath: '/userId' },
|
||||||
audit_log: { partitionKeyPath: '/category', defaultTtl: 90 * 86400 },
|
audit_log: { partitionKeyPath: '/category', defaultTtl: 90 * 86400 },
|
||||||
feature_flags: { partitionKeyPath: '/id' },
|
feature_flags: { partitionKeyPath: '/id' },
|
||||||
|
// Mobile capture primitives (MindLyst-style).
|
||||||
|
memory_items: { partitionKeyPath: '/userId' },
|
||||||
|
daily_briefs: { partitionKeyPath: '/userId' },
|
||||||
|
reflections: { partitionKeyPath: '/userId' },
|
||||||
|
brain_insights: { partitionKeyPath: '/userId' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function initCosmosIfNeeded(): Promise<void> {
|
export async function initCosmosIfNeeded(): Promise<void> {
|
||||||
|
|||||||
63
services/platform-service/src/modules/memory/memory.test.ts
Normal file
63
services/platform-service/src/modules/memory/memory.test.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Tests for memory item schemas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
CreateMemoryItemSchema,
|
||||||
|
ListMemoryItemsQuerySchema,
|
||||||
|
PatchMemoryItemSchema,
|
||||||
|
ReassignMemoryItemSchema,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
describe('ListMemoryItemsQuerySchema', () => {
|
||||||
|
it('accepts defaults', () => {
|
||||||
|
const result = ListMemoryItemsQuerySchema.safeParse({});
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.limit).toBe(50);
|
||||||
|
expect(result.data.offset).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects huge limit', () => {
|
||||||
|
const result = ListMemoryItemsQuerySchema.safeParse({ limit: 9999 });
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CreateMemoryItemSchema', () => {
|
||||||
|
it('requires rawContent', () => {
|
||||||
|
const result = CreateMemoryItemSchema.safeParse({ sourceType: 'text' });
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts minimal valid payload', () => {
|
||||||
|
const result = CreateMemoryItemSchema.safeParse({ rawContent: 'hello world' });
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.data.sourceType).toBe('text');
|
||||||
|
expect(result.data.captureSurface).toBe('app');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ReassignMemoryItemSchema', () => {
|
||||||
|
it('accepts newBrainId', () => {
|
||||||
|
const result = ReassignMemoryItemSchema.safeParse({ newBrainId: 'work' });
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PatchMemoryItemSchema', () => {
|
||||||
|
it('requires reminderAt for set_reminder at route level', () => {
|
||||||
|
const result = PatchMemoryItemSchema.safeParse({ action: 'set_reminder' });
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalid action', () => {
|
||||||
|
const result = PatchMemoryItemSchema.safeParse({ action: 'nope' });
|
||||||
|
expect(result.success).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
101
services/platform-service/src/modules/memory/repository.ts
Normal file
101
services/platform-service/src/modules/memory/repository.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Memory items repository — Cosmos DB CRUD.
|
||||||
|
*
|
||||||
|
* Container: memory_items (partition: /userId)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getContainer } from '../../lib/cosmos.js';
|
||||||
|
import type { MemoryItemDoc } from './types.js';
|
||||||
|
|
||||||
|
function container() {
|
||||||
|
return getContainer('memory_items');
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListMemoryItemsQuery = {
|
||||||
|
productId: string;
|
||||||
|
userId: string;
|
||||||
|
brainId?: string;
|
||||||
|
filter?: 'forgotten' | 'completed_today';
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function list(query: ListMemoryItemsQuery): Promise<{ items: MemoryItemDoc[] }> {
|
||||||
|
const conditions: string[] = ['c.userId = @userId', 'c.productId = @productId'];
|
||||||
|
const params: { name: string; value: string | number | boolean }[] = [
|
||||||
|
{ name: '@userId', value: query.userId },
|
||||||
|
{ name: '@productId', value: query.productId },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (query.brainId) {
|
||||||
|
conditions.push('ARRAY_CONTAINS(c.brainIds, @brainId)');
|
||||||
|
params.push({ name: '@brainId', value: query.brainId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.filter === 'forgotten') {
|
||||||
|
const before = new Date(Date.now() - 48 * 3600 * 1000).toISOString();
|
||||||
|
conditions.push('c.actedOn = false');
|
||||||
|
conditions.push('c.createdAt < @before');
|
||||||
|
params.push({ name: '@before', value: before });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.filter === 'completed_today') {
|
||||||
|
const todayPrefix = new Date().toISOString().slice(0, 10);
|
||||||
|
conditions.push('c.actedOn = true');
|
||||||
|
conditions.push('STARTSWITH(c.actedOnAt, @todayPrefix)');
|
||||||
|
params.push({ name: '@todayPrefix', value: todayPrefix });
|
||||||
|
}
|
||||||
|
|
||||||
|
const where = `WHERE ${conditions.join(' AND ')}`;
|
||||||
|
|
||||||
|
const { resources } = await container()
|
||||||
|
.items.query<MemoryItemDoc>(
|
||||||
|
{
|
||||||
|
query: `SELECT * FROM c ${where} ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit`,
|
||||||
|
parameters: [
|
||||||
|
...params,
|
||||||
|
{ name: '@offset', value: query.offset },
|
||||||
|
{ name: '@limit', value: query.limit },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ partitionKey: query.userId }
|
||||||
|
)
|
||||||
|
.fetchAll();
|
||||||
|
|
||||||
|
return { items: resources ?? [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getById(
|
||||||
|
id: string,
|
||||||
|
userId: string,
|
||||||
|
productId: string
|
||||||
|
): Promise<MemoryItemDoc | null> {
|
||||||
|
try {
|
||||||
|
const { resource } = await container().item(id, userId).read<MemoryItemDoc>();
|
||||||
|
if (!resource) return null;
|
||||||
|
if (resource.productId !== productId) return null;
|
||||||
|
return resource;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(doc: MemoryItemDoc): Promise<MemoryItemDoc> {
|
||||||
|
const { resource } = await container().items.create(doc);
|
||||||
|
return resource as MemoryItemDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function replace(doc: MemoryItemDoc): Promise<MemoryItemDoc> {
|
||||||
|
const { resource } = await container().item(doc.id, doc.userId).replace(doc);
|
||||||
|
return resource as MemoryItemDoc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function remove(id: string, userId: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await container().item(id, userId).delete();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
170
services/platform-service/src/modules/memory/routes.ts
Normal file
170
services/platform-service/src/modules/memory/routes.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
/**
|
||||||
|
* Memory items REST endpoints.
|
||||||
|
*
|
||||||
|
* GET /memory-items — list (optional brainId/filter)
|
||||||
|
* POST /memory-items — create
|
||||||
|
* PUT /memory-items/:id/reassign — reassign to another brain
|
||||||
|
* PATCH /memory-items/:id — mark done/undone, increment nudge, set reminder
|
||||||
|
* DELETE /memory-items/:id — delete
|
||||||
|
*
|
||||||
|
* Container: memory_items (partition key: /userId)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { DEFAULT_PRODUCT_ID } from '../../lib/product-config.js';
|
||||||
|
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
|
||||||
|
import { extractAuth } from '../../lib/auth.js';
|
||||||
|
import * as repo from './repository.js';
|
||||||
|
import {
|
||||||
|
CreateMemoryItemSchema,
|
||||||
|
ListMemoryItemsQuerySchema,
|
||||||
|
PatchMemoryItemSchema,
|
||||||
|
ReassignMemoryItemSchema,
|
||||||
|
type MemoryItemDoc,
|
||||||
|
type TriageResult,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
function defaultTriage(rawContent: string): TriageResult {
|
||||||
|
return {
|
||||||
|
contentType: 'memory',
|
||||||
|
summary: rawContent.slice(0, 200),
|
||||||
|
urgencyScore: 0.3,
|
||||||
|
emotionScore: 0,
|
||||||
|
confidenceScore: 0.5,
|
||||||
|
suggestedBrainId: 'global',
|
||||||
|
entities: [],
|
||||||
|
suggestedActions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function memoryRoutes(app: FastifyInstance) {
|
||||||
|
// List memory items
|
||||||
|
app.get('/memory-items', async req => {
|
||||||
|
const auth = await extractAuth(req);
|
||||||
|
const parsed = ListMemoryItemsQuerySchema.safeParse(req.query);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
const q = parsed.data;
|
||||||
|
const pid = q.productId || DEFAULT_PRODUCT_ID;
|
||||||
|
|
||||||
|
const { items } = await repo.list({
|
||||||
|
productId: pid,
|
||||||
|
userId: auth.sub,
|
||||||
|
brainId: q.brainId,
|
||||||
|
filter: q.filter,
|
||||||
|
limit: q.limit,
|
||||||
|
offset: q.offset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { items, limit: q.limit, offset: q.offset };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create memory item
|
||||||
|
app.post('/memory-items', async (req, reply) => {
|
||||||
|
const auth = await extractAuth(req);
|
||||||
|
const parsed = CreateMemoryItemSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = parsed.data;
|
||||||
|
const pid = input.productId || DEFAULT_PRODUCT_ID;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const triage = input.triageResult ?? defaultTriage(input.rawContent);
|
||||||
|
const brainIds = input.brainIds && input.brainIds.length > 0 ? input.brainIds : [triage.suggestedBrainId];
|
||||||
|
|
||||||
|
const doc: MemoryItemDoc = {
|
||||||
|
id: `mem_${Date.now()}_${randomUUID()}`,
|
||||||
|
productId: pid,
|
||||||
|
userId: auth.sub,
|
||||||
|
sourceType: input.sourceType,
|
||||||
|
captureSurface: input.captureSurface,
|
||||||
|
rawContent: input.rawContent,
|
||||||
|
triageResult: triage,
|
||||||
|
brainIds,
|
||||||
|
...(input.reminderAt && { reminderAt: input.reminderAt }),
|
||||||
|
actedOn: false,
|
||||||
|
actedOnAt: null,
|
||||||
|
nudgeCount: 0,
|
||||||
|
userCorrection: null,
|
||||||
|
isSensitive: input.isSensitive ?? false,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await repo.create(doc);
|
||||||
|
reply.code(201);
|
||||||
|
return created;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reassign
|
||||||
|
app.put('/memory-items/:id/reassign', async req => {
|
||||||
|
const auth = await extractAuth(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
const parsed = ReassignMemoryItemSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pid = (req.query as { productId?: string }).productId || DEFAULT_PRODUCT_ID;
|
||||||
|
const item = await repo.getById(id, auth.sub, pid);
|
||||||
|
if (!item) throw new NotFoundError('Memory item not found');
|
||||||
|
|
||||||
|
const updated: MemoryItemDoc = {
|
||||||
|
...item,
|
||||||
|
brainIds: [parsed.data.newBrainId],
|
||||||
|
userCorrection: parsed.data.newBrainId,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
return repo.replace(updated);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Patch actions
|
||||||
|
app.patch('/memory-items/:id', async req => {
|
||||||
|
const auth = await extractAuth(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
const parsed = PatchMemoryItemSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pid = (req.query as { productId?: string }).productId || DEFAULT_PRODUCT_ID;
|
||||||
|
const item = await repo.getById(id, auth.sub, pid);
|
||||||
|
if (!item) throw new NotFoundError('Memory item not found');
|
||||||
|
|
||||||
|
const action = parsed.data.action;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
let updated: MemoryItemDoc = { ...item, updatedAt: now };
|
||||||
|
if (action === 'mark_done') {
|
||||||
|
updated = { ...updated, actedOn: true, actedOnAt: now };
|
||||||
|
} else if (action === 'mark_undone') {
|
||||||
|
updated = { ...updated, actedOn: false, actedOnAt: null };
|
||||||
|
} else if (action === 'increment_nudge') {
|
||||||
|
updated = { ...updated, nudgeCount: item.nudgeCount + 1 };
|
||||||
|
} else if (action === 'set_reminder') {
|
||||||
|
if (!parsed.data.reminderAt) {
|
||||||
|
throw new BadRequestError('reminderAt is required for set_reminder');
|
||||||
|
}
|
||||||
|
updated = { ...updated, reminderAt: parsed.data.reminderAt };
|
||||||
|
}
|
||||||
|
|
||||||
|
return repo.replace(updated);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
app.delete('/memory-items/:id', async req => {
|
||||||
|
const auth = await extractAuth(req);
|
||||||
|
const { id } = req.params as { id: string };
|
||||||
|
const pid = (req.query as { productId?: string }).productId || DEFAULT_PRODUCT_ID;
|
||||||
|
|
||||||
|
const item = await repo.getById(id, auth.sub, pid);
|
||||||
|
if (!item) throw new NotFoundError('Memory item not found');
|
||||||
|
|
||||||
|
await repo.remove(id, auth.sub);
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
}
|
||||||
90
services/platform-service/src/modules/memory/types.ts
Normal file
90
services/platform-service/src/modules/memory/types.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* MindLyst-style "memory items" — persisted capture + triage results.
|
||||||
|
*
|
||||||
|
* Container: memory_items (partition key: /userId)
|
||||||
|
*
|
||||||
|
* This is intentionally product-agnostic: every doc includes productId.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const SourceTypeSchema = z.enum(['voice', 'image', 'link', 'email', 'text']);
|
||||||
|
|
||||||
|
export const CaptureSurfaceSchema = z.enum([
|
||||||
|
'app',
|
||||||
|
'widget',
|
||||||
|
'share_sheet',
|
||||||
|
'siri',
|
||||||
|
'email',
|
||||||
|
'web',
|
||||||
|
'notification_reply',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const ContentTypeSchema = z.enum(['task', 'reminder', 'memory', 'idea', 'risk', 'reference']);
|
||||||
|
|
||||||
|
export const TriageResultSchema = z.object({
|
||||||
|
contentType: ContentTypeSchema,
|
||||||
|
summary: z.string().min(1).max(2000),
|
||||||
|
urgencyScore: z.number().min(0).max(1),
|
||||||
|
emotionScore: z.number().min(-1).max(1),
|
||||||
|
confidenceScore: z.number().min(0).max(1),
|
||||||
|
suggestedBrainId: z.string().min(1).max(128),
|
||||||
|
entities: z.array(z.string().min(1).max(128)).default([]),
|
||||||
|
suggestedActions: z.array(z.string().min(1).max(256)).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ListMemoryItemsQuerySchema = z.object({
|
||||||
|
productId: z.string().min(1).max(64).optional(),
|
||||||
|
brainId: z.string().min(1).max(128).optional(),
|
||||||
|
filter: z.enum(['forgotten', 'completed_today']).optional(),
|
||||||
|
limit: z.coerce.number().int().min(1).max(200).default(50),
|
||||||
|
offset: z.coerce.number().int().min(0).max(50_000).default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateMemoryItemSchema = z.object({
|
||||||
|
productId: z.string().min(1).max(64).optional(),
|
||||||
|
sourceType: SourceTypeSchema.default('text'),
|
||||||
|
captureSurface: CaptureSurfaceSchema.default('app'),
|
||||||
|
rawContent: z.string().min(1).max(50_000),
|
||||||
|
triageResult: TriageResultSchema.optional(),
|
||||||
|
brainIds: z.array(z.string().min(1).max(128)).optional(),
|
||||||
|
isSensitive: z.boolean().optional(),
|
||||||
|
reminderAt: z
|
||||||
|
.string()
|
||||||
|
.refine(v => !Number.isNaN(Date.parse(v)), 'reminderAt must be an ISO date string')
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ReassignMemoryItemSchema = z.object({
|
||||||
|
newBrainId: z.string().min(1).max(128),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PatchMemoryItemSchema = z.object({
|
||||||
|
action: z.enum(['mark_done', 'mark_undone', 'increment_nudge', 'set_reminder']),
|
||||||
|
reminderAt: z
|
||||||
|
.string()
|
||||||
|
.refine(v => !Number.isNaN(Date.parse(v)), 'reminderAt must be an ISO date string')
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TriageResult = z.infer<typeof TriageResultSchema>;
|
||||||
|
|
||||||
|
export type MemoryItemDoc = {
|
||||||
|
id: string;
|
||||||
|
productId: string;
|
||||||
|
userId: string;
|
||||||
|
sourceType: z.infer<typeof SourceTypeSchema>;
|
||||||
|
captureSurface: z.infer<typeof CaptureSurfaceSchema>;
|
||||||
|
rawContent: string;
|
||||||
|
triageResult: TriageResult;
|
||||||
|
brainIds: string[];
|
||||||
|
reminderAt?: string;
|
||||||
|
actedOn: boolean;
|
||||||
|
actedOnAt: string | null;
|
||||||
|
nudgeCount: number;
|
||||||
|
userCorrection: string | null;
|
||||||
|
isSensitive: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
@ -39,6 +39,7 @@ import { stripeRoutes } from './modules/stripe/routes.js';
|
|||||||
import { itemRoutes } from './modules/items/routes.js';
|
import { itemRoutes } from './modules/items/routes.js';
|
||||||
import { commentRoutes } from './modules/comments/routes.js';
|
import { commentRoutes } from './modules/comments/routes.js';
|
||||||
import { voteRoutes } from './modules/votes/routes.js';
|
import { voteRoutes } from './modules/votes/routes.js';
|
||||||
|
import { memoryRoutes } from './modules/memory/routes.js';
|
||||||
import { publicRoutes } from './modules/public/routes.js';
|
import { publicRoutes } from './modules/public/routes.js';
|
||||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||||
import { config } from './lib/config.js';
|
import { config } from './lib/config.js';
|
||||||
@ -97,6 +98,8 @@ await app.register(stripeRoutes, { prefix: '/api' });
|
|||||||
await app.register(itemRoutes, { prefix: '/api' });
|
await app.register(itemRoutes, { prefix: '/api' });
|
||||||
await app.register(commentRoutes, { prefix: '/api' });
|
await app.register(commentRoutes, { prefix: '/api' });
|
||||||
await app.register(voteRoutes, { prefix: '/api' });
|
await app.register(voteRoutes, { prefix: '/api' });
|
||||||
|
// Mobile capture modules
|
||||||
|
await app.register(memoryRoutes, { prefix: '/api' });
|
||||||
// Public routes — no auth, registered at top level
|
// Public routes — no auth, registered at top level
|
||||||
await app.register(publicRoutes, { prefix: '/api' });
|
await app.register(publicRoutes, { prefix: '/api' });
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user