@compas/store
Advanced tools
Comparing version 0.0.137 to 0.0.138
@@ -120,3 +120,2 @@ import * as stdlib from "@compas/stdlib"; | ||
export interface MigrateFile { | ||
namespace: string; | ||
number: number; | ||
@@ -136,3 +135,2 @@ repeatable: boolean; | ||
files: MigrateFile[]; | ||
namespaces: string[]; | ||
storedHashes: Record<string, string>; | ||
@@ -139,0 +137,0 @@ sql: Postgres; |
@@ -1,3 +0,1 @@ | ||
import { dirnameForModule } from "@compas/stdlib"; | ||
export { | ||
@@ -53,4 +51,2 @@ newMinioClient, | ||
export const migrations = `${dirnameForModule(import.meta)}/migrations`; | ||
export { structure as storeStructure } from "./src/generated/common/structure.js"; | ||
@@ -57,0 +53,0 @@ export { queries as storeQueries } from "./src/generated/database/index.js"; |
{ | ||
"name": "@compas/store", | ||
"version": "0.0.137", | ||
"version": "0.0.138", | ||
"description": "Postgres & S3-compatible wrappers for common things", | ||
@@ -17,3 +17,3 @@ "main": "./index.js", | ||
"dependencies": { | ||
"@compas/stdlib": "0.0.137", | ||
"@compas/stdlib": "0.0.138", | ||
"@types/minio": "7.0.7", | ||
@@ -45,3 +45,3 @@ "mime-types": "2.1.30", | ||
}, | ||
"gitHead": "855cf3b108f158b3f9dd8ee0777b1ab8d7a1a5a4" | ||
"gitHead": "3052deb49d7bf1fa6e26b8fc3375a8dd06012668" | ||
} |
@@ -951,14 +951,13 @@ // Generated by @compas/code-gen | ||
joinQb.append(query`LEFT JOIN LATERAL ( | ||
SELECT array_remove(array_agg(to_jsonb(fg.*) || jsonb_build_object(${query([ | ||
SELECT ARRAY (SELECT to_jsonb(fg.*) || jsonb_build_object(${query([ | ||
joinedKeys.join(","), | ||
])}) ORDER BY ${fileGroupOrderBy( | ||
])}) | ||
${internalQueryFileGroup(builder.children, query`AND fg."parent" = fg2."id"`)} | ||
ORDER BY ${fileGroupOrderBy( | ||
builder.children.orderBy, | ||
builder.children.orderBySpec, | ||
"fg.", | ||
)}), NULL) as "result" | ||
${internalQueryFileGroup(builder.children, query`AND fg."parent" = fg2."id"`)} | ||
GROUP BY fg2."id" | ||
ORDER BY fg2."id" | ||
)} | ||
${offsetLimitQb} | ||
) as "fg_fg_1" ON TRUE`); | ||
) as result) as "fg_fg_1" ON TRUE`); | ||
} | ||
@@ -1185,14 +1184,13 @@ return query` | ||
joinQb.append(query`LEFT JOIN LATERAL ( | ||
SELECT array_remove(array_agg(to_jsonb(fg2.*) || jsonb_build_object(${query([ | ||
SELECT ARRAY (SELECT to_jsonb(fg2.*) || jsonb_build_object(${query([ | ||
joinedKeys.join(","), | ||
])}) ORDER BY ${fileGroupOrderBy( | ||
])}) | ||
${internalQueryFileGroup2(builder.children, query`AND fg2."parent" = fg."id"`)} | ||
ORDER BY ${fileGroupOrderBy( | ||
builder.children.orderBy, | ||
builder.children.orderBySpec, | ||
"fg2.", | ||
)}), NULL) as "result" | ||
${internalQueryFileGroup2(builder.children, query`AND fg2."parent" = fg."id"`)} | ||
GROUP BY fg."id" | ||
ORDER BY fg."id" | ||
)} | ||
${offsetLimitQb} | ||
) as "fg_fg_1" ON TRUE`); | ||
) as result) as "fg_fg_1" ON TRUE`); | ||
} | ||
@@ -1199,0 +1197,0 @@ return query` |
@@ -740,9 +740,5 @@ // Generated by @compas/code-gen | ||
joinQb.append(query`LEFT JOIN LATERAL ( | ||
SELECT array_remove(array_agg(to_jsonb(fgv.*) || jsonb_build_object(${query([ | ||
SELECT ARRAY (SELECT to_jsonb(fgv.*) || jsonb_build_object(${query([ | ||
joinedKeys.join(","), | ||
])}) ORDER BY ${fileGroupViewOrderBy( | ||
builder.children.orderBy, | ||
builder.children.orderBySpec, | ||
"fgv.", | ||
)}), NULL) as "result" | ||
])}) | ||
${internalQueryFileGroupView( | ||
@@ -752,6 +748,9 @@ builder.children, | ||
)} | ||
GROUP BY fgv2."id" | ||
ORDER BY fgv2."id" | ||
ORDER BY ${fileGroupViewOrderBy( | ||
builder.children.orderBy, | ||
builder.children.orderBySpec, | ||
"fgv.", | ||
)} | ||
${offsetLimitQb} | ||
) as "fgv_fgv_1" ON TRUE`); | ||
) as result) as "fgv_fgv_1" ON TRUE`); | ||
} | ||
@@ -981,9 +980,5 @@ return query` | ||
joinQb.append(query`LEFT JOIN LATERAL ( | ||
SELECT array_remove(array_agg(to_jsonb(fgv2.*) || jsonb_build_object(${query([ | ||
SELECT ARRAY (SELECT to_jsonb(fgv2.*) || jsonb_build_object(${query([ | ||
joinedKeys.join(","), | ||
])}) ORDER BY ${fileGroupViewOrderBy( | ||
builder.children.orderBy, | ||
builder.children.orderBySpec, | ||
"fgv2.", | ||
)}), NULL) as "result" | ||
])}) | ||
${internalQueryFileGroupView2( | ||
@@ -993,6 +988,9 @@ builder.children, | ||
)} | ||
GROUP BY fgv."id" | ||
ORDER BY fgv."id" | ||
ORDER BY ${fileGroupViewOrderBy( | ||
builder.children.orderBy, | ||
builder.children.orderBySpec, | ||
"fgv2.", | ||
)} | ||
${offsetLimitQb} | ||
) as "fgv_fgv_1" ON TRUE`); | ||
) as result) as "fgv_fgv_1" ON TRUE`); | ||
} | ||
@@ -999,0 +997,0 @@ return query` |
import { createHash } from "crypto"; | ||
import { existsSync } from "fs"; | ||
import { readdir, readFile } from "fs/promises"; | ||
import { pathToFileURL } from "url"; | ||
import { | ||
AppError, | ||
dirnameForModule, | ||
environment, | ||
pathJoin, | ||
} from "@compas/stdlib"; | ||
import { AppError, environment, pathJoin } from "@compas/stdlib"; | ||
/** | ||
* @typedef {object} MigrationFile | ||
* @property {number} number | ||
* @property {boolean} repeatable | ||
* @property {string} name | ||
* @property {string} fullPath | ||
* @property {boolean} isMigrated | ||
* @property {string} source | ||
* @property {string} hash | ||
*/ | ||
/** | ||
* Create a new migration context, resolves all migrations and collects the current | ||
@@ -29,22 +34,6 @@ * migration state. | ||
// Automatically add this package to the migrations, | ||
// and make sure it is at the front | ||
const storeMigrationIndex = migrations.namespaces.indexOf("@compas/store"); | ||
if (storeMigrationIndex === -1) { | ||
const { migrationFiles, namespaces } = await readMigrationsDir( | ||
`${dirnameForModule(import.meta)}/../migrations`, | ||
"@compas/store", | ||
[], | ||
); | ||
migrations.migrationFiles.push(...migrationFiles); | ||
migrations.namespaces = [].concat(namespaces, migrations.namespaces); | ||
} else if (storeMigrationIndex !== 0) { | ||
migrations.namespaces.splice(storeMigrationIndex, 1); | ||
migrations.namespaces.unshift("@compas/store"); | ||
} | ||
const mc = { | ||
files: sortMigrations(migrations.namespaces, migrations.migrationFiles), | ||
namespaces: migrations.namespaces, | ||
files: migrations.sort((a, b) => { | ||
return a.number - b.number; | ||
}), | ||
sql, | ||
@@ -64,2 +53,7 @@ storedHashes: {}, | ||
await syncWithSchemaState(mc); | ||
mc.rebuild = () => rebuildMigrations(mc); | ||
mc.info = () => getMigrationsToBeApplied(mc); | ||
mc.do = () => runMigrations(mc); | ||
return mc; | ||
@@ -93,4 +87,11 @@ } catch (error) { | ||
* @returns {{ | ||
* migrationQueue: ({ namespace: string, name: string, number: number, repeatable: | ||
* boolean}[]), hashChanges: { name: string, number: number, namespace: string }[] | ||
* migrationQueue: { | ||
* name: string, | ||
* number: number, | ||
* repeatable: boolean | ||
* }[], | ||
* hashChanges: { | ||
* name: string, | ||
* number: number, | ||
* }[] | ||
* }} | ||
@@ -100,3 +101,2 @@ */ | ||
const migrationQueue = filterMigrationsToBeApplied(mc).map((it) => ({ | ||
namespace: it.namespace, | ||
name: it.name, | ||
@@ -109,8 +109,4 @@ number: it.number, | ||
for (const it of mc.files) { | ||
if ( | ||
it.isMigrated && | ||
mc.storedHashes[`${it.namespace}-${it.number}`] !== it.hash | ||
) { | ||
if (it.isMigrated && mc.storedHashes[it.number] !== it.hash) { | ||
hashChanges.push({ | ||
namespace: it.namespace, | ||
name: it.name, | ||
@@ -156,3 +152,2 @@ number: it.number, | ||
message: "Could not run migration", | ||
namespace: current?.namespace, | ||
number: current?.number, | ||
@@ -177,3 +172,36 @@ name: current?.name, | ||
/** | ||
* Rebuild migration table state based on the known migration files | ||
* | ||
* @since 0.1.0 | ||
* | ||
* @param {MigrateContext} mc | ||
* @returns {Promise<undefined>} | ||
*/ | ||
export async function rebuildMigrations(mc) { | ||
try { | ||
await mc.sql.begin(async (sql) => { | ||
await sql`DELETE FROM "migration" WHERE 1 = 1`; | ||
for (const file of mc.files) { | ||
await runInsert(sql, file); | ||
} | ||
}); | ||
} catch (e) { | ||
if ((e.message ?? "").indexOf(`"migration" does not exist`) === -1) { | ||
throw new AppError( | ||
"migrate.rebuild.error", | ||
500, | ||
{ | ||
message: "No migrations applied yet, can't rebuild migration table.", | ||
}, | ||
e, | ||
); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
} | ||
/** | ||
* @param {MigrateContext} mc | ||
* @returns {MigrateFile[]} | ||
@@ -186,6 +214,3 @@ */ | ||
result.push(f); | ||
} else if ( | ||
mc.storedHashes[`${f.namespace}-${f.number}`] !== f.hash && | ||
f.repeatable | ||
) { | ||
} else if (mc.storedHashes[f.number] !== f.hash && f.repeatable) { | ||
result.push(f); | ||
@@ -223,11 +248,5 @@ } | ||
function runInsert(sql, migration) { | ||
return sql` | ||
INSERT INTO migration ${sql( | ||
migration, | ||
"namespace", | ||
"name", | ||
"number", | ||
"hash", | ||
)} | ||
`; | ||
return sql`INSERT INTO "migration" (namespace, number, name, hash) VALUES (${ | ||
environment.APP_NAME ?? "compas" | ||
}, ${migration.number}, ${migration.name}, ${migration.hash});`; | ||
} | ||
@@ -243,7 +262,5 @@ | ||
rows = await mc.sql` | ||
SELECT DISTINCT ON (namespace, number) namespace, | ||
number, | ||
hash | ||
SELECT DISTINCT ON (number) number, hash | ||
FROM migration | ||
ORDER BY namespace, number, "createdAt" DESC | ||
ORDER BY number, "createdAt" DESC | ||
`; | ||
@@ -264,17 +281,11 @@ } catch (e) { | ||
const migrationData = {}; | ||
const numbers = []; | ||
for (const row of rows) { | ||
if (!migrationData[row.namespace]) { | ||
migrationData[row.namespace] = []; | ||
} | ||
migrationData[row.namespace].push(row.number); | ||
numbers.push(Number(row.number)); | ||
mc.storedHashes[`${row.namespace}-${row.number}`] = row.hash; | ||
mc.storedHashes[Number(row.number)] = row.hash; | ||
} | ||
for (const mF of mc.files) { | ||
if ( | ||
migrationData[mF.namespace] && | ||
migrationData[mF.namespace].indexOf(mF.number) !== -1 | ||
) { | ||
if (numbers.includes(mF.number)) { | ||
mF.isMigrated = true; | ||
@@ -286,3 +297,3 @@ } | ||
/** | ||
* @param sql | ||
* @param {Postgres} sql | ||
*/ | ||
@@ -301,7 +312,14 @@ async function acquireLock(sql) { | ||
async function readMigrationFilesForNamespace( | ||
directory, | ||
migrationFiles, | ||
namespace, | ||
) { | ||
/** | ||
* | ||
* @param directory | ||
* @returns {Promise<MigrationFile[]>} | ||
*/ | ||
async function readMigrationsDir(directory) { | ||
if (!existsSync(directory)) { | ||
return []; | ||
} | ||
const migrationFiles = []; | ||
const files = await readdir(directory); | ||
@@ -320,3 +338,2 @@ | ||
migrationFiles.push({ | ||
namespace, | ||
number, | ||
@@ -331,131 +348,7 @@ repeatable, | ||
} | ||
} | ||
/** | ||
* | ||
* @param directory | ||
* @param {string} namespace | ||
* @param {string[]} namespaces | ||
* @returns {Promise<{migrationFiles: [], namespaces: [*]}>} | ||
*/ | ||
async function readMigrationsDir( | ||
directory, | ||
namespace = environment.APP_NAME, | ||
namespaces = [], | ||
) { | ||
if (!existsSync(directory)) { | ||
return { | ||
namespaces: [], | ||
migrationFiles: [], | ||
}; | ||
} | ||
const migrationFiles = []; | ||
const namespacePath = pathJoin(directory, "namespaces.txt"); | ||
if (existsSync(namespacePath)) { | ||
const rawNamespaces = await readFile(namespacePath, "utf-8"); | ||
const subNamespaces = rawNamespaces | ||
.split("\n") | ||
.map((it) => it.trim()) | ||
.filter((it) => it.length > 0); | ||
for (const ns of subNamespaces) { | ||
// Stop recurse if we already handled it, or if the current namespace is | ||
// fetched again | ||
if (namespaces.includes(ns)) { | ||
continue; | ||
} | ||
if (ns === namespace) { | ||
namespaces.push(ns); | ||
await readMigrationFilesForNamespace(directory, migrationFiles, ns); | ||
continue; | ||
} | ||
// Either same level in node_modules | ||
const directPath = pathJoin(process.cwd(), "node_modules", ns); | ||
// Or a level deeper | ||
const indirectPath = pathJoin(directory, "../node_modules", ns); | ||
const subPath = !existsSync(directPath) | ||
? existsSync(indirectPath) | ||
? indirectPath | ||
: new Error( | ||
`Could not determine import path of '${ns}', while searching for migration files.`, | ||
) | ||
: directPath; | ||
// Quick hack | ||
if (typeof subPath !== "string") { | ||
throw subPath; | ||
} | ||
// Use the package.json to find the package entrypoint | ||
// Only supporting simple { exports: "file.js" }, { exports: { default: | ||
// "file.js" } or { main: "file.js" } | ||
const subPackageJson = JSON.parse( | ||
await readFile(pathJoin(subPath, "package.json"), "utf8"), | ||
); | ||
const exportedItems = await import( | ||
pathToFileURL( | ||
pathJoin( | ||
subPath, | ||
subPackageJson?.exports?.default ?? | ||
(typeof subPackageJson?.exports === "string" | ||
? subPackageJson?.exports | ||
: undefined) ?? | ||
subPackageJson?.main ?? | ||
"index.js", | ||
), | ||
) | ||
); | ||
if (exportedItems && exportedItems.migrations) { | ||
const subResult = await readMigrationsDir( | ||
exportedItems.migrations, | ||
ns, | ||
namespaces, | ||
); | ||
migrationFiles.push(...subResult.migrationFiles); | ||
} | ||
} | ||
} | ||
// We might have loaded the migration files already | ||
if (namespaces.includes(namespace)) { | ||
return { | ||
migrationFiles, | ||
namespaces, | ||
}; | ||
} | ||
namespaces.push(namespace); | ||
await readMigrationFilesForNamespace(directory, migrationFiles, namespace); | ||
return { | ||
migrationFiles, | ||
namespaces, | ||
}; | ||
return migrationFiles; | ||
} | ||
/** | ||
* @param namespaces | ||
* @param files | ||
*/ | ||
function sortMigrations(namespaces, files) { | ||
return files.sort((a, b) => { | ||
const namespaceResult = | ||
namespaces.indexOf(a.namespace) - namespaces.indexOf(b.namespace); | ||
if (namespaceResult !== 0) { | ||
return namespaceResult; | ||
} | ||
return a.number < b.number; | ||
}); | ||
} | ||
/** | ||
* @param fileName | ||
@@ -473,3 +366,2 @@ */ | ||
filePattern.lastIndex = 0; | ||
if (!filePattern.test(fileName)) { | ||
@@ -476,0 +368,0 @@ throw new Error( |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
417923
30
13482
+ Added@compas/stdlib@0.0.138(transitive)
+ Added@types/node@15.6.0(transitive)
+ Addeddotenv@10.0.0(transitive)
- Removed@compas/stdlib@0.0.137(transitive)
- Removed@types/node@15.3.0(transitive)
- Removeddotenv@9.0.2(transitive)
Updated@compas/stdlib@0.0.138