fix(cosmos): init containers on startup for local compose
This commit is contained in:
parent
db9b21c36d
commit
95b45a9fd3
@ -70,6 +70,8 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- PORT=4001
|
||||
# Local/dev convenience: ensure Cosmos DB + containers exist.
|
||||
- COSMOS_AUTO_INIT=true
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.growth.rule=PathPrefix(`/api/invitations`) || PathPrefix(`/api/referrals`) || PathPrefix(`/api/promos`)'
|
||||
@ -92,6 +94,8 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- PORT=4002
|
||||
# Local/dev convenience: ensure Cosmos DB + containers exist.
|
||||
- COSMOS_AUTO_INIT=true
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.billing.rule=PathPrefix(`/api/subscriptions`) || PathPrefix(`/api/payments`) || PathPrefix(`/api/usage`) || PathPrefix(`/api/plans`) || PathPrefix(`/api/licenses`) || PathPrefix(`/api/stripe`)'
|
||||
@ -114,6 +118,8 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- PORT=4003
|
||||
# Local/dev convenience: ensure Cosmos DB + containers exist.
|
||||
- COSMOS_AUTO_INIT=true
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.platform.rule=PathPrefix(`/api/auth`) || PathPrefix(`/api/audit`) || PathPrefix(`/api/notifications`) || PathPrefix(`/api/flags`) || PathPrefix(`/api/ratelimit`) || PathPrefix(`/api/blob`)'
|
||||
@ -136,6 +142,8 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
- PORT=4004
|
||||
# Local/dev convenience: ensure Cosmos DB + containers exist.
|
||||
- COSMOS_AUTO_INIT=true
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.tracker.rule=PathPrefix(`/api/items`) || PathPrefix(`/api/tracker`) || PathPrefix(`/public`)'
|
||||
|
||||
@ -462,8 +462,8 @@ The following gaps were identified by scanning every import in the actual codeba
|
||||
- [x] **5.13** Update **user-dashboard-web** `Dockerfile` (pre-copy + dummy env vars)
|
||||
- [x] **5.14** Update **tracker-dashboard-web** `Dockerfile` (pre-copy)
|
||||
- [x] **5.15** `docker-compose.yml` updated with `context: .` + `dockerfile:` paths
|
||||
- [ ] **5.16** Run `docker compose build` — blocked by corporate proxy SSL (not a code issue)
|
||||
- [ ] **5.17** Run `docker compose up -d` — blocked (requires 5.16)
|
||||
- [x] **5.16** Run `docker compose build` — verified on home network
|
||||
- [x] **5.17** Run `docker compose up -d` — verified on home network
|
||||
|
||||
**Alternative (if team grows):** Publish to GitHub Packages first (Phase 7.1), then Docker builds resolve via `npm install` with registry auth — eliminates the pre-copy step entirely.
|
||||
|
||||
@ -494,15 +494,15 @@ The following gaps were identified by scanning every import in the actual codeba
|
||||
- [ ] **6.9** Build MindLyst KMP: blocked behind SSL proxy (passes on home network)
|
||||
- [x] **6.10** Build MindLyst web: `npx next build` passes
|
||||
- [x] **6.11** Run common-plat tests: `pnpm test` — **281 tests pass** across 13 test suites
|
||||
- [ ] **6.12** Docker compose full stack: `docker compose up -d` → all healthy
|
||||
- [x] **6.12** Docker compose full stack: `docker compose up -d` → all healthy
|
||||
|
||||
### End-to-End Flow Tests
|
||||
|
||||
- [ ] **6.13** Test: Admin login → create user → view user list
|
||||
- [ ] **6.14** Test: User login → view profile → update settings
|
||||
- [ ] **6.15** Test: Tracker login → create item → add comment → vote
|
||||
- [ ] **6.16** Test: Public roadmap page → vote → submit idea
|
||||
- [ ] **6.17** Test: Service health checks via monitoring script
|
||||
- [x] **6.15** Test: Tracker login → create item → add comment → vote
|
||||
- [x] **6.16** Test: Public roadmap page → vote → submit idea
|
||||
- [x] **6.17** Test: Service health checks via monitoring script
|
||||
|
||||
### Documentation Updates
|
||||
|
||||
@ -530,8 +530,8 @@ The following gaps were identified by scanning every import in the actual codeba
|
||||
> **Goal:** Deferred improvements that add value but aren't required for initial extraction.
|
||||
|
||||
- [ ] **7.1** Publish `@bytelyst/*` packages to GitHub Packages (private npm registry)
|
||||
- [ ] **7.2** Add Changesets for automated version management and changelogs
|
||||
- [ ] **7.3** Create reusable GitHub Actions workflow templates for service CI
|
||||
- [x] **7.2** Add Changesets for automated version management and changelogs
|
||||
- [x] **7.3** Create reusable GitHub Actions workflow templates for service CI
|
||||
- [x] **7.4** Add `@bytelyst/blob` package (extract blob storage client + SAS generation)
|
||||
- [x] **7.5** Add `@bytelyst/monitoring` package (health check aggregator)
|
||||
- [x] **7.6** Add `@bytelyst/testing` package (shared test utilities, mock factories) — created with 10 tests
|
||||
@ -555,10 +555,10 @@ The following gaps were identified by scanning every import in the actual codeba
|
||||
| **3A** | `@bytelyst/api-client` | 17 | 17 | ✅ Complete |
|
||||
| **3B** | `@bytelyst/react-auth` (24 consumer files) | 28 | 25 | ✅ Admin uses factory; user/tracker keep custom |
|
||||
| **4** | `@bytelyst/design-tokens` (4 platforms) | 24 | 23 | ✅ CSS synced to MindLyst; CONTRIBUTING updated; visual verify pending |
|
||||
| **5** | CI/CD + Docker (pre-copy strategy) | 23 | 21 | ⚠️ All Dockerfiles rewritten, CI workflows created; Docker build blocked by proxy |
|
||||
| **6** | Verification + docs + cleanup | 28 | 21 | ✅ Docs updated, depcheck done, git clean; E2E needs services |
|
||||
| **5** | CI/CD + Docker (pre-copy strategy) | 23 | 23 | ✅ Docker build + compose up verified on home network |
|
||||
| **6** | Verification + docs + cleanup | 28 | 25 | ⚠️ Remaining E2E: admin + user portal flows |
|
||||
| **7** | Future enhancements (+testing pkg) | 10 | 3 | 🔲 @bytelyst/testing (10 tests) + token pre-commit hook + AGENTS updated |
|
||||
| **Total** | **10 packages (+1 bonus: logger)** | **278** | **252** | **~91% complete** |
|
||||
| **Total** | **10 packages (+1 bonus: logger)** | **278** | **257** | **~92% complete** |
|
||||
|
||||
### Bonus Package (not in original roadmap)
|
||||
|
||||
|
||||
@ -22,11 +22,13 @@
|
||||
- 2026-02-15: Verified `extraction-service` Dockerfile build (`docker compose build extraction-service`)
|
||||
- 2026-02-15: Added Changesets (`.changeset/`) for versioning + changelog automation (**7.2**)
|
||||
- 2026-02-15: Added reusable GitHub Actions workflow template (`.github/workflows/reusable-pnpm-workspace.yml`) (**7.3**)
|
||||
- 2026-02-15: Unblocked platform/tracker Cosmos usage by adding dev auto-init for missing DB/containers (services call `initializeAllContainers()`)
|
||||
- 2026-02-15: Verified auth + tracker E2E via container-run smoke: register/login/me; create item/comment/vote; public roadmap vote + submit idea (**6.15**, **6.16**)
|
||||
|
||||
## Prereqs (Local)
|
||||
|
||||
- [x] Create local `.env` from `.env.example` (do not commit the file)
|
||||
- [ ] Set real `.env` values so auth + CRUD flows work (Cosmos + JWT at minimum)
|
||||
- [x] Set real `.env` values so auth + CRUD flows work (Cosmos + JWT at minimum)
|
||||
- [x] Ensure Docker Desktop is running (required for `docker compose build` / `up`)
|
||||
|
||||
## Execution Order (Suggested)
|
||||
@ -73,8 +75,8 @@ Note: MindLyst KMP build verification is tracked in `docs/workstreams/MOBILE_WOR
|
||||
- [x] **6.12** Docker compose full stack: `docker compose up -d` -> all healthy
|
||||
- [ ] **6.13** Test: Admin login -> create user -> view user list
|
||||
- [ ] **6.14** Test: User login -> view profile -> update settings
|
||||
- [ ] **6.15** Test: Tracker login -> create item -> add comment -> vote
|
||||
- [ ] **6.16** Test: Public roadmap page -> vote -> submit idea
|
||||
- [x] **6.15** Test: Tracker login -> create item -> add comment -> vote (verified via tracker-service APIs)
|
||||
- [x] **6.16** Test: Public roadmap page -> vote -> submit idea (verified via tracker-service public APIs)
|
||||
- [x] **6.17** Test: Service health checks via monitoring script (docker-network health probes pass)
|
||||
|
||||
Publishing + repo hygiene
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* and createIfNotExists support.
|
||||
*/
|
||||
|
||||
import { Container, PartitionKeyDefinition } from '@azure/cosmos';
|
||||
import { Container, PartitionKeyDefinition, type Database } from '@azure/cosmos';
|
||||
import { getCosmosClient, getDatabase } from './client.js';
|
||||
import type { ContainerConfig } from './types.js';
|
||||
|
||||
@ -44,16 +44,73 @@ export function getRegisteredContainer(name: string): Container {
|
||||
export async function initializeAllContainers(): Promise<void> {
|
||||
const client = getCosmosClient();
|
||||
const dbId = process.env.COSMOS_DATABASE || 'lysnrai';
|
||||
const { database } = await client.databases.createIfNotExists({ id: dbId });
|
||||
const database = await createDatabaseSafe(client, dbId);
|
||||
|
||||
for (const [name, config] of _registry.entries()) {
|
||||
await database.containers.createIfNotExists({
|
||||
await createContainerSafe(database, name, config);
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => globalThis.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function isCosmosConflict(err: unknown): boolean {
|
||||
const e = err as { code?: number; statusCode?: number; message?: string } | null;
|
||||
if (!e) return false;
|
||||
if (e.code === 409 || e.statusCode === 409) return true;
|
||||
return (e.message || '').toLowerCase().includes('already exists');
|
||||
}
|
||||
|
||||
function isCosmosNotFound(err: unknown): boolean {
|
||||
const e = err as { code?: number; statusCode?: number; message?: string } | null;
|
||||
if (!e) return false;
|
||||
if (e.code === 404 || e.statusCode === 404) return true;
|
||||
return (e.message || '').toLowerCase().includes('not found');
|
||||
}
|
||||
|
||||
async function createDatabaseSafe(
|
||||
client: ReturnType<typeof getCosmosClient>,
|
||||
dbId: string
|
||||
): Promise<Database> {
|
||||
try {
|
||||
const { database } = await client.databases.createIfNotExists({ id: dbId });
|
||||
return database;
|
||||
} catch (err) {
|
||||
// createIfNotExists is not atomic; concurrent create can race and throw a conflict.
|
||||
if (isCosmosConflict(err)) return client.database(dbId);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function createContainerSafe(
|
||||
database: Database,
|
||||
name: string,
|
||||
config: ContainerConfig
|
||||
): Promise<void> {
|
||||
const payload = {
|
||||
id: name,
|
||||
partitionKey: {
|
||||
paths: [config.partitionKeyPath],
|
||||
} as PartitionKeyDefinition,
|
||||
...(config.defaultTtl != null && { defaultTtl: config.defaultTtl }),
|
||||
});
|
||||
};
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
await database.containers.createIfNotExists(payload);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (isCosmosConflict(err)) return; // Container was created by another process.
|
||||
|
||||
// Sometimes the database/container metadata isn't immediately visible after creation.
|
||||
if (isCosmosNotFound(err) && attempt < 2) {
|
||||
await sleep(250 * (attempt + 1));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
services/billing-service/src/lib/cosmos-init.ts
Normal file
28
services/billing-service/src/lib/cosmos-init.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { initializeAllContainers, registerContainers } from '@bytelyst/cosmos';
|
||||
import type { ContainerConfig } from '@bytelyst/cosmos';
|
||||
import { config } from './config.js';
|
||||
|
||||
const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
plans: { partitionKeyPath: '/id' },
|
||||
subscriptions: { partitionKeyPath: '/userId' },
|
||||
payments: { partitionKeyPath: '/userId' },
|
||||
licenses: { partitionKeyPath: '/userId' },
|
||||
usage_daily: { partitionKeyPath: '/userId', defaultTtl: 365 * 86400 },
|
||||
};
|
||||
|
||||
export async function initCosmosIfNeeded(): Promise<void> {
|
||||
registerContainers(CONTAINER_DEFS);
|
||||
|
||||
const shouldInit = config.NODE_ENV !== 'production' || process.env.COSMOS_AUTO_INIT === 'true';
|
||||
if (!shouldInit) return;
|
||||
|
||||
try {
|
||||
await initializeAllContainers();
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[billing-service] Cosmos containers ensured');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[billing-service] Cosmos init failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
@ -11,8 +11,11 @@ import { usageRoutes } from './modules/usage/routes.js';
|
||||
import { planRoutes } from './modules/plans/routes.js';
|
||||
import { licenseRoutes } from './modules/licenses/routes.js';
|
||||
import { stripeRoutes } from './modules/stripe/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
|
||||
await initCosmosIfNeeded();
|
||||
|
||||
const app = await createServiceApp({
|
||||
name: 'billing-service',
|
||||
version: '0.1.0',
|
||||
|
||||
25
services/growth-service/src/lib/cosmos-init.ts
Normal file
25
services/growth-service/src/lib/cosmos-init.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { initializeAllContainers, registerContainers } from '@bytelyst/cosmos';
|
||||
import type { ContainerConfig } from '@bytelyst/cosmos';
|
||||
import { config } from './config.js';
|
||||
|
||||
const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
invitation_codes: { partitionKeyPath: '/id' },
|
||||
referrals: { partitionKeyPath: '/referrerId' },
|
||||
};
|
||||
|
||||
export async function initCosmosIfNeeded(): Promise<void> {
|
||||
registerContainers(CONTAINER_DEFS);
|
||||
|
||||
const shouldInit = config.NODE_ENV !== 'production' || process.env.COSMOS_AUTO_INIT === 'true';
|
||||
if (!shouldInit) return;
|
||||
|
||||
try {
|
||||
await initializeAllContainers();
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[growth-service] Cosmos containers ensured');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[growth-service] Cosmos init failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
@ -9,8 +9,11 @@ import { createServiceApp, startService } from '@bytelyst/fastify-core';
|
||||
import { invitationRoutes } from './modules/invitations/routes.js';
|
||||
import { referralRoutes } from './modules/referrals/routes.js';
|
||||
import { promoRoutes } from './modules/promos/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
|
||||
await initCosmosIfNeeded();
|
||||
|
||||
const app = await createServiceApp({
|
||||
name: 'growth-service',
|
||||
version: '0.1.0',
|
||||
|
||||
28
services/platform-service/src/lib/cosmos-init.ts
Normal file
28
services/platform-service/src/lib/cosmos-init.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { initializeAllContainers, registerContainers } from '@bytelyst/cosmos';
|
||||
import type { ContainerConfig } from '@bytelyst/cosmos';
|
||||
import { config } from './config.js';
|
||||
|
||||
const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
users: { partitionKeyPath: '/id' },
|
||||
devices: { partitionKeyPath: '/userId' },
|
||||
notification_prefs: { partitionKeyPath: '/userId' },
|
||||
audit_log: { partitionKeyPath: '/category', defaultTtl: 90 * 86400 },
|
||||
feature_flags: { partitionKeyPath: '/id' },
|
||||
};
|
||||
|
||||
export async function initCosmosIfNeeded(): Promise<void> {
|
||||
registerContainers(CONTAINER_DEFS);
|
||||
|
||||
const shouldInit = config.NODE_ENV !== 'production' || process.env.COSMOS_AUTO_INIT === 'true';
|
||||
if (!shouldInit) return;
|
||||
|
||||
try {
|
||||
await initializeAllContainers();
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[platform-service] Cosmos containers ensured');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[platform-service] Cosmos init failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
@ -12,8 +12,11 @@ import { notificationRoutes } from './modules/notifications/routes.js';
|
||||
import { flagRoutes } from './modules/flags/routes.js';
|
||||
import { rateLimitRoutes } from './modules/ratelimit/routes.js';
|
||||
import { blobRoutes } from './modules/blob/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
|
||||
await initCosmosIfNeeded();
|
||||
|
||||
const app = await createServiceApp({
|
||||
name: 'platform-service',
|
||||
version: '0.1.0',
|
||||
|
||||
26
services/tracker-service/src/lib/cosmos-init.ts
Normal file
26
services/tracker-service/src/lib/cosmos-init.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { initializeAllContainers, registerContainers } from '@bytelyst/cosmos';
|
||||
import type { ContainerConfig } from '@bytelyst/cosmos';
|
||||
import { config } from './config.js';
|
||||
|
||||
const CONTAINER_DEFS: Record<string, ContainerConfig> = {
|
||||
tracker_items: { partitionKeyPath: '/id' },
|
||||
tracker_comments: { partitionKeyPath: '/id' },
|
||||
tracker_votes: { partitionKeyPath: '/id' },
|
||||
};
|
||||
|
||||
export async function initCosmosIfNeeded(): Promise<void> {
|
||||
registerContainers(CONTAINER_DEFS);
|
||||
|
||||
const shouldInit = config.NODE_ENV !== 'production' || process.env.COSMOS_AUTO_INIT === 'true';
|
||||
if (!shouldInit) return;
|
||||
|
||||
try {
|
||||
await initializeAllContainers();
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[tracker-service] Cosmos containers ensured');
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[tracker-service] Cosmos init failed: ${msg}`);
|
||||
}
|
||||
}
|
||||
@ -11,8 +11,11 @@ import { itemRoutes } from './modules/items/routes.js';
|
||||
import { commentRoutes } from './modules/comments/routes.js';
|
||||
import { voteRoutes } from './modules/votes/routes.js';
|
||||
import { publicRoutes } from './modules/public/routes.js';
|
||||
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
|
||||
import { config } from './lib/config.js';
|
||||
|
||||
await initCosmosIfNeeded();
|
||||
|
||||
const app = await createServiceApp({
|
||||
name: 'tracker-service',
|
||||
version: '0.1.0',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user