directus-deploy-cli 0.1.0 → 0.2.0
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.
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import { join, extname
|
|
2
|
+
import { join, extname } from "node:path";
|
|
3
3
|
import { splitSql } from "../sql.js";
|
|
4
|
-
// Apply migrations/*.sql files idempotently. Tracks applied set in a
|
|
5
|
-
// `lola_deploy_migrations` table on the target so we don't re-run history on
|
|
6
|
-
// every deploy (the backend's convention is that migrations are idempotent
|
|
7
|
-
// via `IF NOT EXISTS`, so re-running is safe, but pointless and slow).
|
|
8
|
-
//
|
|
9
|
-
// Uses Directus's /raw-query/execute endpoint — the same one the backend has
|
|
10
|
-
// used for years. Not Postgres-agnostic, but nothing else in this repo is
|
|
11
|
-
// either (we're a Directus deploy tool). Callers on a fresh env need the
|
|
12
|
-
// endpoint deployed as an extension; we detect the 404 and skip cleanly.
|
|
13
|
-
//
|
|
14
|
-
// Requires: /raw-query/execute deployed on the target. We PROBE it first so a
|
|
15
|
-
// missing extension yields a clean skip rather than N confusing errors.
|
|
16
|
-
const TRACKER_TABLE = "lola_deploy_migrations";
|
|
17
|
-
const CREATE_TRACKER = `
|
|
18
|
-
CREATE TABLE IF NOT EXISTS ${TRACKER_TABLE} (
|
|
19
|
-
filename TEXT PRIMARY KEY,
|
|
20
|
-
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
21
|
-
);
|
|
22
|
-
`;
|
|
23
4
|
async function rawQuery(client, sql) {
|
|
24
5
|
try {
|
|
25
6
|
const r = (await client.postRaw("/raw-query/execute", { query: sql }));
|
|
@@ -28,35 +9,37 @@ async function rawQuery(client, sql) {
|
|
|
28
9
|
return r;
|
|
29
10
|
}
|
|
30
11
|
catch (e) {
|
|
31
|
-
// 404 = extension not installed
|
|
32
12
|
const msg = e.message;
|
|
33
13
|
if (msg.includes(" 404 "))
|
|
34
14
|
return null;
|
|
35
15
|
throw e;
|
|
36
16
|
}
|
|
37
17
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
18
|
+
function stripLeadingComments(s) {
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (true) {
|
|
21
|
+
while (i < s.length && /\s/.test(s[i]))
|
|
22
|
+
i += 1;
|
|
23
|
+
if (s[i] === "-" && s[i + 1] === "-") {
|
|
24
|
+
const nl = s.indexOf("\n", i);
|
|
25
|
+
if (nl === -1)
|
|
26
|
+
return "";
|
|
27
|
+
i = nl + 1;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (s[i] === "/" && s[i + 1] === "*") {
|
|
31
|
+
const end = s.indexOf("*/", i);
|
|
32
|
+
if (end === -1)
|
|
33
|
+
return "";
|
|
34
|
+
i = end + 2;
|
|
35
|
+
continue;
|
|
52
36
|
}
|
|
37
|
+
break;
|
|
53
38
|
}
|
|
54
|
-
return
|
|
39
|
+
return s.slice(i);
|
|
55
40
|
}
|
|
56
41
|
export async function reconcileMigrations(input) {
|
|
57
42
|
const results = [];
|
|
58
|
-
// Probe for the endpoint. Failing here is not an error — a fresh env just
|
|
59
|
-
// hasn't installed the raw-query extension yet.
|
|
60
43
|
const probe = await rawQuery(input.client, "SELECT 1 AS ok");
|
|
61
44
|
if (probe === null) {
|
|
62
45
|
results.push({
|
|
@@ -72,31 +55,13 @@ export async function reconcileMigrations(input) {
|
|
|
72
55
|
entries = await readdir(input.migrationsDir);
|
|
73
56
|
}
|
|
74
57
|
catch {
|
|
75
|
-
return results;
|
|
58
|
+
return results;
|
|
76
59
|
}
|
|
77
60
|
const files = entries.filter((f) => extname(f) === ".sql" && !f.startsWith("_")).sort();
|
|
78
61
|
if (files.length === 0)
|
|
79
62
|
return results;
|
|
80
|
-
// Ensure the tracker exists (idempotent).
|
|
81
|
-
if (!input.opts.dryRun) {
|
|
82
|
-
const created = await rawQuery(input.client, CREATE_TRACKER);
|
|
83
|
-
if (!created?.success) {
|
|
84
|
-
results.push({
|
|
85
|
-
kind: "migrations",
|
|
86
|
-
label: `migrations/${TRACKER_TABLE}`,
|
|
87
|
-
action: "failed",
|
|
88
|
-
reason: "could not create tracker table",
|
|
89
|
-
});
|
|
90
|
-
return results;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
const applied = (await fetchAppliedFilenames(input.client)) ?? new Set();
|
|
94
63
|
for (const filename of files) {
|
|
95
64
|
const label = `migrations/${filename}`;
|
|
96
|
-
if (applied.has(filename)) {
|
|
97
|
-
results.push({ kind: "migrations", label, action: "unchanged" });
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
65
|
const full = join(input.migrationsDir, filename);
|
|
101
66
|
let raw;
|
|
102
67
|
try {
|
|
@@ -106,34 +71,6 @@ export async function reconcileMigrations(input) {
|
|
|
106
71
|
results.push({ kind: "migrations", label, action: "failed", reason: e.message });
|
|
107
72
|
continue;
|
|
108
73
|
}
|
|
109
|
-
// Filter out statements that are purely comments/whitespace after their
|
|
110
|
-
// leading `--` or `/* ... */` blocks. Anything else (including statements
|
|
111
|
-
// that BEGIN with a comment but have real SQL after) stays.
|
|
112
|
-
const stripLeadingComments = (s) => {
|
|
113
|
-
let i = 0;
|
|
114
|
-
// repeat until no comments remain at the head
|
|
115
|
-
// eslint-disable-next-line no-constant-condition
|
|
116
|
-
while (true) {
|
|
117
|
-
while (i < s.length && /\s/.test(s[i]))
|
|
118
|
-
i += 1;
|
|
119
|
-
if (s[i] === "-" && s[i + 1] === "-") {
|
|
120
|
-
const nl = s.indexOf("\n", i);
|
|
121
|
-
if (nl === -1)
|
|
122
|
-
return "";
|
|
123
|
-
i = nl + 1;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (s[i] === "/" && s[i + 1] === "*") {
|
|
127
|
-
const end = s.indexOf("*/", i);
|
|
128
|
-
if (end === -1)
|
|
129
|
-
return "";
|
|
130
|
-
i = end + 2;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
break;
|
|
134
|
-
}
|
|
135
|
-
return s.slice(i);
|
|
136
|
-
};
|
|
137
74
|
const statements = splitSql(raw).filter((s) => {
|
|
138
75
|
const stripped = stripLeadingComments(s.trim()).trim();
|
|
139
76
|
return stripped.length > 0 && stripped !== ";";
|
|
@@ -151,7 +88,6 @@ export async function reconcileMigrations(input) {
|
|
|
151
88
|
});
|
|
152
89
|
continue;
|
|
153
90
|
}
|
|
154
|
-
// Apply. If any statement fails, report and stop (don't record as applied).
|
|
155
91
|
let failedReason = null;
|
|
156
92
|
for (const stmt of statements) {
|
|
157
93
|
const r = await rawQuery(input.client, stmt);
|
|
@@ -165,16 +101,6 @@ export async function reconcileMigrations(input) {
|
|
|
165
101
|
results.push({ kind: "migrations", label, action: "failed", reason: failedReason });
|
|
166
102
|
continue;
|
|
167
103
|
}
|
|
168
|
-
const recorded = await rawQuery(input.client, `INSERT INTO ${TRACKER_TABLE} (filename) VALUES ('${basename(filename).replace(/'/g, "''")}')`);
|
|
169
|
-
if (!recorded?.success) {
|
|
170
|
-
results.push({
|
|
171
|
-
kind: "migrations",
|
|
172
|
-
label,
|
|
173
|
-
action: "failed",
|
|
174
|
-
reason: "applied statements but could not record in tracker",
|
|
175
|
-
});
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
104
|
results.push({ kind: "migrations", label, action: "created" });
|
|
179
105
|
}
|
|
180
106
|
return results;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/reconcilers/migrations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/reconcilers/migrations.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "directus-deploy-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Per-entity, non-atomic Directus deployment tool. Reconciles collections/fields/relations/roles/policies/permissions/flows/operations/migrations/seeds from git to any environment.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|