@spfn/core 0.2.0-beta.71 → 0.2.0-beta.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/dist/config/index.d.ts +30 -0
- package/dist/config/index.js +8 -0
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +133 -2
- package/dist/db/index.js +198 -4
- package/dist/db/index.js.map +1 -1
- package/dist/server/index.d.ts +94 -2
- package/dist/server/index.js +135 -13
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/db/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import net from 'net';
|
|
|
5
5
|
import postgres from 'postgres';
|
|
6
6
|
import { QueryError, ConnectionError, DeadlockError, TransactionError, ConstraintViolationError, DuplicateEntryError, DatabaseError } from '@spfn/core/errors';
|
|
7
7
|
import { parseNumber, parseBoolean } from '@spfn/core/env';
|
|
8
|
-
import { existsSync,
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
|
|
9
9
|
import { join, dirname, basename } from 'path';
|
|
10
10
|
import { bigserial, timestamp, bigint, uuid as uuid$1, text, jsonb, pgSchema } from 'drizzle-orm/pg-core';
|
|
11
|
+
import { createHash, randomUUID } from 'crypto';
|
|
12
|
+
import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
|
|
11
13
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
12
14
|
import { createMiddleware } from 'hono/factory';
|
|
13
|
-
import { randomUUID } from 'crypto';
|
|
14
|
-
import { sql, count as count$1, lt, gt, and, desc, asc, eq } from 'drizzle-orm';
|
|
15
15
|
|
|
16
16
|
// src/db/manager/factory.ts
|
|
17
17
|
function parseUniqueViolation(message) {
|
|
@@ -1548,6 +1548,200 @@ function getSchemaInfo(packageName) {
|
|
|
1548
1548
|
scope
|
|
1549
1549
|
};
|
|
1550
1550
|
}
|
|
1551
|
+
var discoveryLogger = logger.child("@spfn/core:migrations");
|
|
1552
|
+
function functionMigrationsTable(packageName) {
|
|
1553
|
+
return `__spfn_fn_${packageName.replace("@spfn/", "")}_migrations`;
|
|
1554
|
+
}
|
|
1555
|
+
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
1556
|
+
const spfnDir = join(cwd, "node_modules", "@spfn");
|
|
1557
|
+
if (!existsSync(spfnDir)) {
|
|
1558
|
+
return [];
|
|
1559
|
+
}
|
|
1560
|
+
const functions = [];
|
|
1561
|
+
for (const pkg of readdirSync(spfnDir)) {
|
|
1562
|
+
const packagePath = join(spfnDir, pkg);
|
|
1563
|
+
const packageJsonPath = join(packagePath, "package.json");
|
|
1564
|
+
if (!existsSync(packageJsonPath)) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
try {
|
|
1568
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
1569
|
+
const migrationsConfig = packageJson.spfn?.migrations;
|
|
1570
|
+
if (!migrationsConfig) {
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
const migrationsDir = join(packagePath, migrationsConfig.dir);
|
|
1574
|
+
if (!existsSync(migrationsDir)) {
|
|
1575
|
+
discoveryLogger.warn(
|
|
1576
|
+
`@spfn/${pkg} declares migrations but the directory is missing: ${migrationsDir}`
|
|
1577
|
+
);
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
functions.push({ packageName: `@spfn/${pkg}`, migrationsDir, packagePath });
|
|
1581
|
+
} catch {
|
|
1582
|
+
discoveryLogger.warn(`Failed to parse package.json for @spfn/${pkg}`);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return functions;
|
|
1586
|
+
}
|
|
1587
|
+
function readMigrationEntries(migrationsDir, packageName) {
|
|
1588
|
+
const journalPath = join(migrationsDir, "meta", "_journal.json");
|
|
1589
|
+
return existsSync(journalPath) ? readJournalEntries(migrationsDir, journalPath, packageName) : readFolderEntries(migrationsDir, packageName);
|
|
1590
|
+
}
|
|
1591
|
+
function readJournalEntries(migrationsDir, journalPath, packageName) {
|
|
1592
|
+
let journal;
|
|
1593
|
+
try {
|
|
1594
|
+
journal = JSON.parse(readFileSync(journalPath, "utf-8"));
|
|
1595
|
+
} catch {
|
|
1596
|
+
journal = {};
|
|
1597
|
+
}
|
|
1598
|
+
if (!Array.isArray(journal.entries)) {
|
|
1599
|
+
throw new Error(`${packageName}: invalid migration journal at ${journalPath}`);
|
|
1600
|
+
}
|
|
1601
|
+
const entries = [...journal.entries];
|
|
1602
|
+
entries.sort((a, b) => a.idx - b.idx);
|
|
1603
|
+
return entries.map((entry) => {
|
|
1604
|
+
if (typeof entry?.tag !== "string" || typeof entry?.when !== "number") {
|
|
1605
|
+
throw new Error(`${packageName}: invalid journal entry in ${journalPath}`);
|
|
1606
|
+
}
|
|
1607
|
+
const sqlPath = join(migrationsDir, `${entry.tag}.sql`);
|
|
1608
|
+
if (!existsSync(sqlPath)) {
|
|
1609
|
+
throw new Error(`${packageName}: migration file not found: ${entry.tag}.sql`);
|
|
1610
|
+
}
|
|
1611
|
+
return toEntry(entry.tag, readFileSync(sqlPath, "utf-8"), entry.when);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
function readFolderEntries(migrationsDir, packageName) {
|
|
1615
|
+
const folders = readdirSync(migrationsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).filter((name) => existsSync(join(migrationsDir, name, "migration.sql"))).sort((a, b) => a.localeCompare(b));
|
|
1616
|
+
return folders.map((name) => toEntry(
|
|
1617
|
+
name,
|
|
1618
|
+
readFileSync(join(migrationsDir, name, "migration.sql"), "utf-8"),
|
|
1619
|
+
folderTimestampMillis(name, packageName)
|
|
1620
|
+
));
|
|
1621
|
+
}
|
|
1622
|
+
function folderTimestampMillis(name, packageName) {
|
|
1623
|
+
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(name);
|
|
1624
|
+
if (!match) {
|
|
1625
|
+
throw new Error(`${packageName}: migration folder name must start with a YYYYMMDDHHMMSS timestamp: ${name}`);
|
|
1626
|
+
}
|
|
1627
|
+
const [, year, month, day, hour, minute, second] = match;
|
|
1628
|
+
return Date.UTC(
|
|
1629
|
+
Number(year),
|
|
1630
|
+
Number(month) - 1,
|
|
1631
|
+
Number(day),
|
|
1632
|
+
Number(hour),
|
|
1633
|
+
Number(minute),
|
|
1634
|
+
Number(second)
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
function toEntry(name, content, millis) {
|
|
1638
|
+
return {
|
|
1639
|
+
name,
|
|
1640
|
+
millis,
|
|
1641
|
+
hash: createHash("sha256").update(content).digest("hex"),
|
|
1642
|
+
statements: content.split("--> statement-breakpoint").map((statement) => statement.trim()).filter((statement) => statement.length > 0)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
1646
|
+
var PROJECT_TARGET_NAME = "project (src/server/drizzle)";
|
|
1647
|
+
function filterPendingEntries(entries, lastAppliedMillis, appliedNames) {
|
|
1648
|
+
return entries.filter((entry) => entry.millis > lastAppliedMillis && !appliedNames.has(entry.name));
|
|
1649
|
+
}
|
|
1650
|
+
function migrationTargets(status) {
|
|
1651
|
+
return [...status.packages, ...status.project ? [status.project] : []];
|
|
1652
|
+
}
|
|
1653
|
+
function pendingMigrationTargets(status) {
|
|
1654
|
+
return migrationTargets(status).filter((target) => target.pending > 0);
|
|
1655
|
+
}
|
|
1656
|
+
function countPendingMigrations(status) {
|
|
1657
|
+
return migrationTargets(status).reduce((sum, target) => sum + target.pending, 0);
|
|
1658
|
+
}
|
|
1659
|
+
function toRows(result) {
|
|
1660
|
+
if (Array.isArray(result)) {
|
|
1661
|
+
return result;
|
|
1662
|
+
}
|
|
1663
|
+
const rows = result?.rows;
|
|
1664
|
+
return Array.isArray(rows) ? rows : [];
|
|
1665
|
+
}
|
|
1666
|
+
async function tableExists(db, tableName) {
|
|
1667
|
+
const rows = toRows(await db.execute(sql`
|
|
1668
|
+
SELECT EXISTS (
|
|
1669
|
+
SELECT 1 FROM information_schema.tables
|
|
1670
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName}
|
|
1671
|
+
) AS "exists"`));
|
|
1672
|
+
return rows[0]?.exists === true;
|
|
1673
|
+
}
|
|
1674
|
+
async function readAppliedNames(db, tableName) {
|
|
1675
|
+
const columnRows = toRows(await db.execute(sql`
|
|
1676
|
+
SELECT EXISTS (
|
|
1677
|
+
SELECT 1 FROM information_schema.columns
|
|
1678
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName} AND column_name = 'name'
|
|
1679
|
+
) AS "exists"`));
|
|
1680
|
+
if (columnRows[0]?.exists !== true) {
|
|
1681
|
+
return /* @__PURE__ */ new Set();
|
|
1682
|
+
}
|
|
1683
|
+
const rows = toRows(await db.execute(sql`
|
|
1684
|
+
SELECT name FROM drizzle.${sql.identifier(tableName)} WHERE name IS NOT NULL`));
|
|
1685
|
+
return new Set(rows.map((row) => String(row.name)));
|
|
1686
|
+
}
|
|
1687
|
+
async function collectTargetStatus(db, name, migrationsDir, tableName) {
|
|
1688
|
+
const entries = readMigrationEntries(migrationsDir, name);
|
|
1689
|
+
let lastApplied = 0;
|
|
1690
|
+
let appliedNames = /* @__PURE__ */ new Set();
|
|
1691
|
+
if (await tableExists(db, tableName)) {
|
|
1692
|
+
const rows = toRows(await db.execute(sql`
|
|
1693
|
+
SELECT created_at FROM drizzle.${sql.identifier(tableName)}
|
|
1694
|
+
ORDER BY created_at DESC LIMIT 1`));
|
|
1695
|
+
lastApplied = rows[0]?.created_at ? Number(rows[0].created_at) : 0;
|
|
1696
|
+
appliedNames = await readAppliedNames(db, tableName);
|
|
1697
|
+
}
|
|
1698
|
+
const pendingEntries = filterPendingEntries(entries, lastApplied, appliedNames);
|
|
1699
|
+
return {
|
|
1700
|
+
name,
|
|
1701
|
+
total: entries.length,
|
|
1702
|
+
applied: entries.length - pendingEntries.length,
|
|
1703
|
+
pending: pendingEntries.length,
|
|
1704
|
+
pendingTags: pendingEntries.map((entry) => entry.name)
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
function hasMigrationTargets(cwd = process.cwd()) {
|
|
1708
|
+
return discoverFunctionMigrations(cwd).length > 0 || existsSync(projectMigrationsDir(cwd));
|
|
1709
|
+
}
|
|
1710
|
+
function projectMigrationsDir(cwd = process.cwd()) {
|
|
1711
|
+
return join(cwd, "src", "server", "drizzle");
|
|
1712
|
+
}
|
|
1713
|
+
async function collectMigrationStatus(db, cwd = process.cwd()) {
|
|
1714
|
+
const packages = [];
|
|
1715
|
+
for (const func of discoverFunctionMigrations(cwd)) {
|
|
1716
|
+
packages.push(await collectTargetStatus(
|
|
1717
|
+
db,
|
|
1718
|
+
func.packageName,
|
|
1719
|
+
func.migrationsDir,
|
|
1720
|
+
functionMigrationsTable(func.packageName)
|
|
1721
|
+
));
|
|
1722
|
+
}
|
|
1723
|
+
const projectDir = projectMigrationsDir(cwd);
|
|
1724
|
+
const project = existsSync(projectDir) ? await collectTargetStatus(db, PROJECT_TARGET_NAME, projectDir, PROJECT_MIGRATIONS_TABLE) : null;
|
|
1725
|
+
return { packages, project: project && project.total > 0 ? project : null };
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// src/db/migrations/format.ts
|
|
1729
|
+
var RUN_MIGRATIONS_HINT = "Run: pnpm spfn db migrate";
|
|
1730
|
+
function formatPendingMigrations(targets) {
|
|
1731
|
+
const lines = [];
|
|
1732
|
+
for (const target of targets) {
|
|
1733
|
+
lines.push(`${target.name}: ${target.pending} pending migration(s) (${target.applied}/${target.total} applied)`);
|
|
1734
|
+
for (const tag of target.pendingTags) {
|
|
1735
|
+
lines.push(` - ${tag}`);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return lines;
|
|
1739
|
+
}
|
|
1740
|
+
function pendingMigrationsSummary(targets) {
|
|
1741
|
+
const pending = targets.reduce((sum, target) => sum + target.pending, 0);
|
|
1742
|
+
const names = targets.map((target) => target.name).join(", ");
|
|
1743
|
+
return `${pending} pending migration(s) in ${names}`;
|
|
1744
|
+
}
|
|
1551
1745
|
var txLogger = logger.child("@spfn/core:transaction");
|
|
1552
1746
|
var asyncContext = new AsyncLocalStorage();
|
|
1553
1747
|
function getTransactionContext() {
|
|
@@ -2337,6 +2531,6 @@ var BaseRepository = class {
|
|
|
2337
2531
|
}
|
|
2338
2532
|
};
|
|
2339
2533
|
|
|
2340
|
-
export { BaseRepository, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, count, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, enumText, findMany, findOne, forceReconnectDatabase, foreignKey, fromPostgresError, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, id, initDatabase, isConnectionLevelError, onAfterCommit, optionalForeignKey, packageNameToSchema, publishingFields, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
2534
|
+
export { BaseRepository, PROJECT_MIGRATIONS_TABLE, PROJECT_TARGET_NAME, RUN_MIGRATIONS_HINT, RepositoryError, Transactional, auditFields, checkConnection, closeDatabase, collectMigrationStatus, count, countPendingMigrations, create, createDatabaseConnection, createDatabaseFromEnv, createMany, createSchema, deleteMany, deleteOne, detectDialect, discoverFunctionMigrations, enumText, filterPendingEntries, findMany, findOne, forceReconnectDatabase, foreignKey, formatPendingMigrations, fromPostgresError, functionMigrationsTable, generateDrizzleConfigFile, getDatabase, getDatabaseInfo, getDrizzleConfig, getSchemaInfo, getTransaction, getTransactionContext, hasMigrationTargets, id, initDatabase, isConnectionLevelError, migrationTargets, onAfterCommit, optionalForeignKey, packageNameToSchema, pendingMigrationTargets, pendingMigrationsSummary, projectMigrationsDir, publishingFields, readMigrationEntries, reportDatabaseError, resetConnectionErrorCounter, runInTransaction, runWithTransaction, setDatabase, setDatabaseProvider, softDelete, timestamps, typedJsonb, updateMany, updateOne, upsert, utcTimestamp, uuid, verificationTimestamp };
|
|
2341
2535
|
//# sourceMappingURL=index.js.map
|
|
2342
2536
|
//# sourceMappingURL=index.js.map
|