refactor(dashboards): migrate admin-web to @bytelyst/datastore

This commit is contained in:
saravanakumardb1 2026-03-02 01:49:41 -08:00
parent 78cb13d9c3
commit fc1fef9c70
11 changed files with 216 additions and 249 deletions

View File

@ -30,6 +30,7 @@
"@bytelyst/auth": "workspace:*",
"@bytelyst/config": "workspace:*",
"@bytelyst/cosmos": "workspace:*",
"@bytelyst/datastore": "workspace:*",
"@bytelyst/errors": "workspace:*",
"@bytelyst/extraction": "workspace:*",
"@bytelyst/logger": "workspace:*",

View File

@ -9,7 +9,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { logError } from '@/lib/logger';
import { getCurrentUser } from '@/lib/auth-server';
import { getContainer } from '@/lib/cosmos';
import { getCollection } from '@/lib/datastore';
import { getRequestProductId } from '@/lib/product-config';
interface CohortRow {
cohortWeek: string; // e.g. "2026-W05"
@ -46,19 +46,13 @@ export async function GET(req: NextRequest) {
const productId = getRequestProductId(req);
// Get users created in the last N weeks
const sinceDate = new Date(Date.now() - weeks * 7 * 86400000).toISOString().slice(0, 10);
const usersContainer = getContainer('users');
const { resources: users } = await usersContainer.items
.query<{ id: string; createdAt: string }>({
query:
'SELECT c.id, c.createdAt FROM c ' +
'WHERE c.productId = @pid AND c.createdAt >= @since ' +
'ORDER BY c.createdAt ASC',
parameters: [
{ name: '@pid', value: productId },
{ name: '@since', value: sinceDate },
],
})
.fetchAll();
const usersCollection = getCollection('users', '/id');
const users = await usersCollection.rawQuery<{ id: string; createdAt: string }>(
'SELECT c.id, c.createdAt FROM c ' +
'WHERE c.productId = @pid AND c.createdAt >= @since ' +
'ORDER BY c.createdAt ASC',
{ pid: productId, since: sinceDate }
);
if (users.length === 0) {
return NextResponse.json({ cohorts: [], totalUsers: 0 });
}
@ -74,16 +68,11 @@ export async function GET(req: NextRequest) {
cohortMap.get(week)!.userIds.push(u.id);
}
// Get all usage records for these users
const usageContainer = getContainer('usage_daily');
const { resources: usageRecords } = await usageContainer.items
.query<{ userId: string; date: string }>({
query: 'SELECT c.userId, c.date FROM c ' + 'WHERE c.productId = @pid AND c.date >= @since',
parameters: [
{ name: '@pid', value: productId },
{ name: '@since', value: sinceDate },
],
})
.fetchAll();
const usageCollection = getCollection('usage_daily', '/userId');
const usageRecords = await usageCollection.rawQuery<{ userId: string; date: string }>(
'SELECT c.userId, c.date FROM c WHERE c.productId = @pid AND c.date >= @since',
{ pid: productId, since: sinceDate }
);
// Build a set of (userId, date) for quick lookup
const userActiveDates = new Map<string, Set<string>>();
for (const r of usageRecords) {

View File

@ -7,7 +7,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth-server';
import { getContainer } from '@/lib/cosmos';
import { getCollection } from '@/lib/datastore';
import { getRequestProductId } from '@/lib/product-config';
interface MonthlyRevenue {
@ -45,56 +45,41 @@ export async function GET(req: NextRequest) {
const sinceDate = new Date(now.getFullYear(), now.getMonth() - months, 1).toISOString();
// ---- Active subscriptions for MRR ----
const subsContainer = getContainer('subscriptions');
const { resources: activeSubs } = await subsContainer.items
.query<{ id: string; plan: string; price: number; status: string; createdAt: string }>({
query:
'SELECT c.id, c.plan, c.price, c.status, c.createdAt FROM c ' +
"WHERE c.productId = @pid AND c.status = 'active'",
parameters: [{ name: '@pid', value: productId }],
})
.fetchAll();
const subsCollection = getCollection('subscriptions', '/userId');
const activeSubs = await subsCollection.rawQuery<{
id: string;
plan: string;
price: number;
status: string;
createdAt: string;
}>(
'SELECT c.id, c.plan, c.price, c.status, c.createdAt FROM c ' +
"WHERE c.productId = @pid AND c.status = 'active'",
{ pid: productId }
);
// ---- Canceled subscriptions (last N months) ----
const { resources: canceledSubs } = await subsContainer.items
.query<{ id: string; canceledAt: string }>({
query:
'SELECT c.id, c.canceledAt FROM c ' +
"WHERE c.productId = @pid AND c.status = 'canceled' " +
'AND c.canceledAt >= @since',
parameters: [
{ name: '@pid', value: productId },
{ name: '@since', value: sinceDate },
],
})
.fetchAll();
const canceledSubs = await subsCollection.rawQuery<{ id: string; canceledAt: string }>(
'SELECT c.id, c.canceledAt FROM c ' +
"WHERE c.productId = @pid AND c.status = 'canceled' " +
'AND c.canceledAt >= @since',
{ pid: productId, since: sinceDate }
);
// ---- New subscriptions (last N months) ----
const { resources: newSubs } = await subsContainer.items
.query<{ id: string; createdAt: string }>({
query:
'SELECT c.id, c.createdAt FROM c ' + 'WHERE c.productId = @pid AND c.createdAt >= @since',
parameters: [
{ name: '@pid', value: productId },
{ name: '@since', value: sinceDate },
],
})
.fetchAll();
const newSubs = await subsCollection.rawQuery<{ id: string; createdAt: string }>(
'SELECT c.id, c.createdAt FROM c WHERE c.productId = @pid AND c.createdAt >= @since',
{ pid: productId, since: sinceDate }
);
// ---- Payments (last N months) ----
const paymentsContainer = getContainer('payments');
const { resources: payments } = await paymentsContainer.items
.query<{ amount: number; paidAt: string }>({
query:
'SELECT c.amount, c.paidAt FROM c ' +
"WHERE c.productId = @pid AND c.status = 'succeeded' " +
'AND c.paidAt >= @since',
parameters: [
{ name: '@pid', value: productId },
{ name: '@since', value: sinceDate },
],
})
.fetchAll();
const paymentsCollection = getCollection('payments', '/userId');
const payments = await paymentsCollection.rawQuery<{ amount: number; paidAt: string }>(
'SELECT c.amount, c.paidAt FROM c ' +
"WHERE c.productId = @pid AND c.status = 'succeeded' " +
'AND c.paidAt >= @since',
{ pid: productId, since: sinceDate }
);
// ---- Compute metrics ----
const mrr = activeSubs.reduce((sum, s) => sum + (s.price ?? 0), 0);

View File

@ -1,5 +1,5 @@
import { NextResponse } from 'next/server';
import { getDatabase } from '@/lib/cosmos';
import { isDatastoreHealthy } from '@/lib/datastore';
interface Check {
name: string;
@ -25,15 +25,18 @@ function checkEnvVars(): Check {
return { name: 'env', status: 'pass', message: `${REQUIRED_ENV.length} required vars set` };
}
async function checkCosmos(): Promise<Check> {
async function checkDatastore(): Promise<Check> {
const start = Date.now();
try {
const db = getDatabase();
await db.read();
return { name: 'cosmos', status: 'pass', latencyMs: Date.now() - start };
const healthy = await isDatastoreHealthy();
return {
name: 'datastore',
status: healthy ? 'pass' : 'fail',
latencyMs: Date.now() - start,
};
} catch (err) {
return {
name: 'cosmos',
name: 'datastore',
status: 'fail',
message: err instanceof Error ? err.message : String(err),
latencyMs: Date.now() - start,
@ -45,7 +48,7 @@ export async function GET() {
const checks: Check[] = [];
checks.push(checkEnvVars());
checks.push(await checkCosmos());
checks.push(await checkDatastore());
const overall = checks.every(c => c.status === 'pass') ? 'ok' : 'degraded';

View File

@ -8,9 +8,16 @@
*/
import { NextRequest, NextResponse } from 'next/server';
import { getContainer } from '@/lib/cosmos';
import { getCollection } from '@/lib/datastore';
import { requireAdmin } from '@/lib/auth-server';
import { logError } from '@/lib/logger';
import type { DocumentCollection } from '@bytelyst/datastore';
// Settings docs use userId as partition key and don't have a productId field
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function settingsCollection(): DocumentCollection<any> {
return getCollection('settings', '/userId');
}
const SETTINGS_ID = 'platform_config';
const PARTITION_KEY = '_system';
@ -78,13 +85,8 @@ export async function GET(req: NextRequest) {
const admin = await requireAdmin(req);
if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const container = getContainer('settings');
try {
const { resource } = await container.item(SETTINGS_ID, PARTITION_KEY).read<PlatformSettings>();
if (resource) return NextResponse.json(resource);
} catch {
// Not found — return defaults
}
const settings = await settingsCollection().findById(SETTINGS_ID, PARTITION_KEY);
if (settings) return NextResponse.json(settings);
return NextResponse.json(DEFAULTS);
} catch (error) {
logError('Platform settings GET error', error);
@ -98,15 +100,9 @@ export async function PUT(req: NextRequest) {
if (!admin) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const body = await req.json();
const container = getContainer('settings');
let existing: PlatformSettings = DEFAULTS;
try {
const { resource } = await container.item(SETTINGS_ID, PARTITION_KEY).read<PlatformSettings>();
if (resource) existing = resource;
} catch {
// Use defaults
}
const existingDoc = await settingsCollection().findById(SETTINGS_ID, PARTITION_KEY);
const existing: PlatformSettings = (existingDoc as PlatformSettings) ?? DEFAULTS;
const updated: PlatformSettings = {
...existing,
@ -124,7 +120,7 @@ export async function PUT(req: NextRequest) {
updatedBy: admin.email ?? 'admin',
};
await container.items.upsert(updated);
await settingsCollection().upsert(updated);
return NextResponse.json(updated);
} catch (error) {
logError('Platform settings PUT error', error);

View File

@ -1,20 +1,11 @@
/**
* Azure Cosmos DB client for admin dashboard.
* Uses @bytelyst/cosmos shared package with container registry.
* Azure Cosmos DB container initialization for admin dashboard.
*
* Usage:
* import { getContainer } from "@/lib/cosmos";
* const users = getContainer("users");
* const { resource } = await users.item(id, id).read();
* Data access has moved to @/lib/datastore (cloud-agnostic).
* This file is kept only for the seed route's initializeAllContainers().
*/
import {
getCosmosClient,
getDatabase,
registerContainers,
getRegisteredContainer,
initializeAllContainers,
} from '@bytelyst/cosmos';
import { registerContainers, initializeAllContainers } from '@bytelyst/cosmos';
import type { ContainerConfig } from '@bytelyst/cosmos';
// Container definitions: name → partition key + optional TTL
@ -37,12 +28,4 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
// Register on module load
registerContainers(CONTAINER_DEFS);
/**
* Get a container proxy by name.
* Containers must be pre-created in Azure (or via the seed script).
*/
export function getContainer(name: string) {
return getRegisteredContainer(name);
}
export { getCosmosClient, getDatabase, initializeAllContainers };
export { initializeAllContainers };

View File

@ -0,0 +1,61 @@
/**
* Cloud-agnostic datastore bridge for admin dashboard.
*
* Wraps @bytelyst/datastore with admin-web container registry config.
* Repositories import getCollection() from here instead of getContainer() from cosmos.
*
* Migration: Replace `import { getContainer } from '@/lib/cosmos'`
* with `import { getCollection } from '@/lib/datastore'`
*/
import {
type DatastoreProvider,
type DocumentCollection,
type BaseDocument,
setDatastore,
CosmosDatastoreProvider,
MemoryDatastoreProvider,
} from '@bytelyst/datastore';
let _provider: DatastoreProvider | null = null;
/**
* Initialize the datastore provider.
* Auto-initializes on first getCollection() call.
*/
export function initDatastore(): DatastoreProvider {
if (_provider) return _provider;
const dbProvider = (process.env.DB_PROVIDER || 'cosmos').toLowerCase();
if (dbProvider === 'memory') {
_provider = new MemoryDatastoreProvider();
} else {
_provider = new CosmosDatastoreProvider();
}
setDatastore(_provider);
return _provider;
}
/**
* Get a typed collection from the datastore.
* Drop-in replacement for getContainer() returns a DocumentCollection.
*/
export function getCollection<T extends BaseDocument = BaseDocument>(
name: string,
partitionKeyPath: string = '/id'
): DocumentCollection<T> {
if (!_provider) {
initDatastore();
}
return _provider!.getCollection<T>(name, partitionKeyPath);
}
/**
* Check datastore health.
*/
export async function isDatastoreHealthy(): Promise<boolean> {
if (!_provider) initDatastore();
return _provider!.isHealthy();
}

View File

@ -1,10 +1,13 @@
/** Theme repository for Cosmos DB operations. */
/** Theme repository for cloud-agnostic datastore operations. */
import { getContainer } from '@/lib/cosmos';
import { getCollection } from '@/lib/datastore';
import { Theme, ThemeCreate, ThemeUpdate } from '@/types/theme';
import type { DocumentCollection } from '@bytelyst/datastore';
function container() {
return getContainer('themes');
// Theme docs use id as partition key and don't have a productId field
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function collection(): DocumentCollection<any> {
return getCollection('themes', '/id');
}
export class ThemeRepository {
@ -26,35 +29,28 @@ export class ThemeRepository {
type: 'theme',
};
const { resource } = await container().items.create(newTheme);
const resource = await collection().create(newTheme);
return resource as Theme;
}
async getById(id: string): Promise<Theme | null> {
try {
const { resource } = await container().item(id, id).read();
return resource;
} catch {
return null;
}
return collection().findById(id, id);
}
async getActive(): Promise<Theme | null> {
const query = `
SELECT * FROM c
WHERE c.type = 'theme' AND c.is_active = true
ORDER BY c.is_default DESC, c.created_at DESC
OFFSET 0 LIMIT 1
`;
const { resources } = await container().items.query(query).fetchAll();
return resources[0] || null;
const results = await collection().rawQuery<Theme>(
`SELECT * FROM c
WHERE c.type = 'theme' AND c.is_active = true
ORDER BY c.is_default DESC, c.created_at DESC
OFFSET 0 LIMIT 1`
);
return results[0] || null;
}
async getAll(): Promise<Theme[]> {
const query = "SELECT * FROM c WHERE c.type = 'theme' ORDER BY c.created_at DESC";
const { resources } = await container().items.query(query).fetchAll();
return resources;
return collection().rawQuery<Theme>(
"SELECT * FROM c WHERE c.type = 'theme' ORDER BY c.created_at DESC"
);
}
async update(id: string, update: ThemeUpdate): Promise<Theme | null> {
@ -72,7 +68,7 @@ export class ThemeRepository {
await this.deactivateAllOthers(id);
}
const { resource } = await container().item(id, id).replace(updatedTheme);
const resource = await collection().update(id, id, updatedTheme);
return resource as Theme;
}
@ -90,7 +86,7 @@ export class ThemeRepository {
async delete(id: string): Promise<boolean> {
try {
await container().item(id, id).delete();
await collection().delete(id, id);
return true;
} catch {
return false;
@ -98,23 +94,16 @@ export class ThemeRepository {
}
private async deactivateAllOthers(excludeId: string): Promise<void> {
const query = `
SELECT * FROM c
WHERE c.type = 'theme' AND c.id != @excludeId AND c.is_active = true
`;
const { resources } = await container()
.items.query({ query, parameters: [{ name: '@excludeId', value: excludeId }] })
.fetchAll();
const resources = await collection().rawQuery<Theme>(
'SELECT * FROM c WHERE c.type = @type AND c.id != @excludeId AND c.is_active = true',
{ type: 'theme', excludeId }
);
for (const item of resources) {
await container()
.item(item.id, item.id)
.replace({
...item,
is_active: false,
updated_at: new Date().toISOString(),
});
await collection().update(item.id, item.id, {
is_active: false,
updated_at: new Date().toISOString(),
});
}
}
}

View File

@ -1,14 +1,12 @@
/**
* API token repository direct Cosmos DB CRUD operations.
* API token repository cloud-agnostic datastore operations.
*/
import { SqlQuerySpec } from '@azure/cosmos';
import { getContainer } from '@/lib/cosmos';
import { getCollection } from '@/lib/datastore';
import { PRODUCT_ID } from '@/lib/product-config';
import type { BaseDocument, DocumentCollection } from '@bytelyst/datastore';
export interface ApiTokenDoc {
id: string;
productId: string; // multi-tenancy key from @bytelyst/config
export interface ApiTokenDoc extends BaseDocument {
userId: string;
userName: string;
name: string;
@ -24,8 +22,8 @@ export interface ApiTokenDoc {
export type ApiTokenResponse = Omit<ApiTokenDoc, 'tokenHash'>;
function container() {
return getContainer('api_tokens');
function collection(): DocumentCollection<ApiTokenDoc> {
return getCollection<ApiTokenDoc>('api_tokens', '/userId');
}
function stripHash(doc: ApiTokenDoc): ApiTokenResponse {
@ -35,24 +33,15 @@ function stripHash(doc: ApiTokenDoc): ApiTokenResponse {
}
export async function getTokenById(id: string, userId: string): Promise<ApiTokenDoc | null> {
try {
const { resource } = await container().item(id, userId).read<ApiTokenDoc>();
return resource ?? null;
} catch {
return null;
}
return collection().findById(id, userId);
}
export async function listTokens(limit = 100, productId = PRODUCT_ID): Promise<ApiTokenResponse[]> {
const query: SqlQuerySpec = {
query:
"SELECT * FROM c WHERE c.productId = @productId AND c.status != 'expired' ORDER BY c.createdAt DESC OFFSET 0 LIMIT @limit",
parameters: [
{ name: '@productId', value: productId },
{ name: '@limit', value: limit },
],
};
const { resources } = await container().items.query<ApiTokenDoc>(query).fetchAll();
const resources = await collection().findMany({
filter: { productId, status: { $ne: 'expired' } },
sort: { createdAt: -1 },
limit,
});
return resources.map(stripHash);
}
@ -60,39 +49,29 @@ export async function listTokensByUser(
userId: string,
productId = PRODUCT_ID
): Promise<ApiTokenResponse[]> {
const query: SqlQuerySpec = {
query:
'SELECT * FROM c WHERE c.productId = @productId AND c.userId = @userId ORDER BY c.createdAt DESC',
parameters: [
{ name: '@productId', value: productId },
{ name: '@userId', value: userId },
],
};
const { resources } = await container().items.query<ApiTokenDoc>(query).fetchAll();
const resources = await collection().findMany({
filter: { productId, userId },
sort: { createdAt: -1 },
});
return resources.map(stripHash);
}
export async function createToken(doc: ApiTokenDoc): Promise<ApiTokenResponse> {
const { resource } = await container().items.create<ApiTokenDoc>(doc);
return stripHash(resource!);
const resource = await collection().create(doc);
return stripHash(resource);
}
export async function revokeToken(id: string, userId: string): Promise<boolean> {
const existing = await getTokenById(id, userId);
if (!existing) return false;
await container()
.item(id, userId)
.replace<ApiTokenDoc>({
...existing,
status: 'revoked',
});
await collection().update(id, userId, { status: 'revoked' } as Partial<ApiTokenDoc>);
return true;
}
export async function deleteToken(id: string, userId: string): Promise<boolean> {
try {
await container().item(id, userId).delete();
await collection().delete(id, userId);
return true;
} catch {
return false;
@ -100,7 +79,5 @@ export async function deleteToken(id: string, userId: string): Promise<boolean>
}
export async function countActiveTokens(productId = PRODUCT_ID): Promise<number> {
const query = `SELECT VALUE COUNT(1) FROM c WHERE c.productId = '${productId}' AND c.status = 'active'`;
const { resources } = await container().items.query<number>(query).fetchAll();
return resources[0] ?? 0;
return collection().count({ productId, status: 'active' });
}

View File

@ -1,14 +1,12 @@
/**
* User repository direct Cosmos DB CRUD operations.
* User repository cloud-agnostic datastore operations.
*/
import { SqlQuerySpec } from '@azure/cosmos';
import { getContainer } from '@/lib/cosmos';
import { getCollection } from '@/lib/datastore';
import { PRODUCT_ID } from '@/lib/product-config';
import type { BaseDocument, DocumentCollection } from '@bytelyst/datastore';
export interface UserDoc {
id: string;
productId: string; // multi-tenancy key from @bytelyst/config
export interface UserDoc extends BaseDocument {
email: string;
name: string;
passwordHash: string;
@ -24,8 +22,8 @@ export interface UserDoc {
export type UserResponse = Omit<UserDoc, 'passwordHash'>;
function container() {
return getContainer('users');
function collection(): DocumentCollection<UserDoc> {
return getCollection<UserDoc>('users', '/id');
}
function stripPassword(doc: UserDoc): UserResponse {
@ -35,27 +33,16 @@ function stripPassword(doc: UserDoc): UserResponse {
}
export async function getUserById(id: string): Promise<UserDoc | null> {
try {
const { resource } = await container().item(id, id).read<UserDoc>();
return resource ?? null;
} catch {
return null;
}
return collection().findById(id, id);
}
export async function getUserByEmail(
email: string,
productId = PRODUCT_ID
): Promise<UserDoc | null> {
const query: SqlQuerySpec = {
query: 'SELECT * FROM c WHERE c.productId = @productId AND c.email = @email',
parameters: [
{ name: '@productId', value: productId },
{ name: '@email', value: email.toLowerCase() },
],
};
const { resources } = await container().items.query<UserDoc>(query).fetchAll();
return resources[0] ?? null;
return collection().findOne({
filter: { productId, email: email.toLowerCase() },
});
}
export async function listUsers(
@ -63,25 +50,21 @@ export async function listUsers(
offset = 0,
productId = PRODUCT_ID
): Promise<UserResponse[]> {
const query: SqlQuerySpec = {
query:
'SELECT * FROM c WHERE c.productId = @productId ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit',
parameters: [
{ name: '@productId', value: productId },
{ name: '@offset', value: offset },
{ name: '@limit', value: limit },
],
};
const { resources } = await container().items.query<UserDoc>(query).fetchAll();
const resources = await collection().findMany({
filter: { productId },
sort: { createdAt: -1 },
offset,
limit,
});
return resources.map(stripPassword);
}
export async function createUser(doc: UserDoc): Promise<UserResponse> {
const { resource } = await container().items.create<UserDoc>({
const resource = await collection().create({
...doc,
email: doc.email.toLowerCase(),
});
return stripPassword(resource!);
return stripPassword(resource);
}
export async function updateUser(
@ -91,14 +74,16 @@ export async function updateUser(
const existing = await getUserById(id);
if (!existing) return null;
const updated = { ...existing, ...updates, lastActive: new Date().toISOString() };
const { resource } = await container().item(id, id).replace<UserDoc>(updated);
return stripPassword(resource!);
const updated = await collection().update(id, id, {
...updates,
lastActive: new Date().toISOString(),
});
return stripPassword(updated);
}
export async function deleteUser(id: string): Promise<boolean> {
try {
await container().item(id, id).delete();
await collection().delete(id, id);
return true;
} catch {
return false;
@ -106,20 +91,15 @@ export async function deleteUser(id: string): Promise<boolean> {
}
export async function countUsers(productId = PRODUCT_ID): Promise<number> {
const { resources } = await container()
.items.query<number>({
query: 'SELECT VALUE COUNT(1) FROM c WHERE c.productId = @productId',
parameters: [{ name: '@productId', value: productId }],
})
.fetchAll();
return resources[0] ?? 0;
return collection().count({ productId });
}
export async function countUsersByPlan(productId = PRODUCT_ID): Promise<Record<string, number>> {
const query = `SELECT c.plan, COUNT(1) AS count FROM c WHERE c.productId = '${productId}' GROUP BY c.plan`;
const { resources } = await container()
.items.query<{ plan: string; count: number }>(query)
.fetchAll();
const resources = await collection().aggregate<{ plan: string; count: number }>({
groupBy: 'plan',
aggregations: [{ field: 'id', op: 'count', alias: 'count' }],
filter: { productId },
});
const result: Record<string, number> = { free: 0, pro: 0, enterprise: 0 };
for (const r of resources) {
result[r.plan] = r.count;

3
pnpm-lock.yaml generated
View File

@ -89,6 +89,9 @@ importers:
'@bytelyst/cosmos':
specifier: workspace:*
version: link:../../packages/cosmos
'@bytelyst/datastore':
specifier: workspace:*
version: link:../../packages/datastore
'@bytelyst/errors':
specifier: workspace:*
version: link:../../packages/errors