marcidb-client 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.
Binary file
Binary file
Binary file
Binary file
package/bin.js CHANGED
@@ -8,23 +8,25 @@ import fs from "node:fs";
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
 
11
- const BINS = {
12
- "linux-x64": "marci-generate-linux-x64",
13
- "darwin-arm64": "marci-generate-darwin-arm64",
14
- "win32-x64": "marci-generate-win32-x64.exe",
11
+ // Platform suffix for the prebuilt binaries. `marci-generate` generates TS types,
12
+ // `marci-migrate` — migration files (a separate binary after the migration refactor).
13
+ const PLATFORM = {
14
+ "linux-x64": "linux-x64",
15
+ "darwin-arm64": "darwin-arm64",
16
+ "win32-x64": "win32-x64.exe",
15
17
  };
16
18
 
17
- function binPath() {
19
+ function binPath(base) {
18
20
  const key = `${os.platform()}-${os.arch()}`;
19
- const name = BINS[key];
20
- if (!name) {
21
+ const suffix = PLATFORM[key];
22
+ if (!suffix) {
21
23
  console.error(`marcidb: unsupported platform ${key}`);
22
24
  process.exit(1);
23
25
  }
24
- return path.join(__dirname, "bin", name);
26
+ return path.join(__dirname, "bin", `${base}-${suffix}`);
25
27
  }
26
28
 
27
- // Разбирает позиционные аргументы и флаги (--flag value | --flag)
29
+ // Parses positional arguments and flags (--flag value | --flag)
28
30
  function parseArgs(args) {
29
31
  const positional = [];
30
32
  const flags = {};
@@ -56,14 +58,19 @@ Commands:
56
58
  --name <name> migration name (default: migration)
57
59
  --no-migrations types only, skip the migration file
58
60
 
59
- migrate push <db> [options] Push the schema to a running server (creates the db if absent)
61
+ migrate push <db> [options] Push migration files (.march) to a server; it applies the new ones
60
62
  --url <url> server url (default: http://localhost:3000)
61
- --schema <path> schema file (default: schema.marci)
63
+ --migrations <dir> migrations directory (default: migrations)
64
+
65
+ migrate check <db> [options] Compare the server's current snapshot with your latest migration
66
+ --url <url> server url (default: http://localhost:3000)
67
+ --migrations <dir> migrations directory (default: migrations)
62
68
 
63
69
  Examples:
64
70
  marcidb generate
65
71
  marcidb generate schema.marci --name add_users
66
72
  marcidb migrate push myapp --url http://localhost:3000
73
+ marcidb migrate check myapp
67
74
  `);
68
75
  }
69
76
 
@@ -71,25 +78,24 @@ function cmdGenerate(args) {
71
78
  const { positional, flags } = parseArgs(args);
72
79
  const schema = positional[0] ?? "schema.marci";
73
80
  const output = positional[1] ?? path.join(__dirname, "..", ".marcidb", "client");
74
- const bin = binPath();
75
81
 
76
- // 1) TypeScript-типы (всегда — без них миграция бессмысленна)
82
+ // 1) TypeScript types (always — without them a migration is pointless)
77
83
  if (fs.existsSync(output)) {
78
84
  fs.rmSync(output, { recursive: true, force: true });
79
85
  }
80
86
  console.log(`Generating types from ${schema} into ${output}...`);
81
- execFileSync(bin, ["types", schema, output], { stdio: "inherit" });
87
+ execFileSync(binPath("marci-generate"), ["types", schema, output], { stdio: "inherit" });
82
88
  fs.writeFileSync(path.join(output, "package.json"), `{
83
89
  "name": "marci-client",
84
90
  "main": "index.js",
85
91
  "types": "index.d.ts"
86
92
  }`);
87
93
 
88
- // 2) Файл миграции (если не отключено)
94
+ // 2) Migration file (full snapshot of the version) — via the separate marci-migrate binary
89
95
  if (!flags["no-migrations"]) {
90
96
  const dir = flags.migrations ?? "migrations";
91
97
  const name = flags.name ?? "migration";
92
- execFileSync(bin, ["migration", schema, dir, name], { stdio: "inherit" });
98
+ execFileSync(binPath("marci-migrate"), ["generate", schema, dir, name], { stdio: "inherit" });
93
99
  }
94
100
 
95
101
  console.log("Done.");
@@ -97,34 +103,79 @@ function cmdGenerate(args) {
97
103
 
98
104
  async function cmdMigrate(args) {
99
105
  const [sub, ...rest] = args;
100
- if (sub !== "push") {
101
- console.error(`marcidb migrate: unknown subcommand "${sub ?? ""}" (expected "push")`);
102
- help();
103
- process.exit(1);
106
+ if (sub === "push") return migratePush(rest);
107
+ if (sub === "check") return migrateCheck(rest);
108
+ console.error(`marcidb migrate: unknown subcommand "${sub ?? ""}" (expected "push" or "check")`);
109
+ help();
110
+ process.exit(1);
111
+ }
112
+
113
+ // The server's current snapshot ("" if the DB doesn't exist yet)
114
+ async function fetchServerSnapshot(url, db) {
115
+ const res = await fetch(`${url}/${db}/$snapshot`);
116
+ if (res.ok) return await res.text();
117
+ if (res.status === 404) return "";
118
+ console.error(`Cannot read server snapshot [${res.status}]: ${await res.text()}`);
119
+ process.exit(1);
120
+ }
121
+
122
+ // Smart client: marci-migrate plan (STDIN = server snapshot) → actions of the not-yet-applied migrations
123
+ function planActions(dir, serverSnapshot) {
124
+ try {
125
+ return { out: execFileSync(binPath("marci-migrate"), ["plan", dir], { input: serverSnapshot, encoding: "utf8" }) };
126
+ } catch (e) {
127
+ return { error: (e.stderr || e.message || "").toString().trim() };
104
128
  }
129
+ }
105
130
 
131
+ // Push: the client computes the unapplied tail and sends it to the dumb $migrate (the server just applies it)
132
+ async function migratePush(rest) {
106
133
  const { positional, flags } = parseArgs(rest);
107
134
  const db = positional[0];
108
- if (!db) {
109
- console.error("marcidb migrate push: <db> name required");
135
+ if (!db) { console.error("marcidb migrate push: <db> name required"); process.exit(1); }
136
+
137
+ const url = (flags.url ?? "http://localhost:3000").replace(/\/+$/, "");
138
+ const dir = flags.migrations ?? "migrations";
139
+ if (!fs.existsSync(dir)) {
140
+ console.error(`marcidb migrate push: migrations directory "${dir}" not found — run "marcidb generate" first`);
110
141
  process.exit(1);
111
142
  }
112
143
 
113
- const url = (flags.url ?? "http://localhost:3000").replace(/\/+$/, "");
114
- const schemaPath = flags.schema ?? "schema.marci";
115
- const schemaText = fs.readFileSync(schemaPath, "utf8");
144
+ const serverSnap = await fetchServerSnapshot(url, db);
145
+ const plan = planActions(dir, serverSnap);
146
+ if (plan.error !== undefined) { console.error(`Migration planning failed: ${plan.error}`); process.exit(1); }
147
+ if (!plan.out.trim()) { console.log(`'${db}' is up to date — nothing to push`); return; }
116
148
 
117
149
  const res = await fetch(`${url}/${db}/$migrate`, {
118
150
  method: "POST",
119
151
  headers: { "Content-Type": "text/plain" },
120
- body: schemaText,
152
+ body: plan.out,
121
153
  });
154
+ if (!res.ok) { console.error(`Migration failed [${res.status}]: ${await res.text()}`); process.exit(1); }
155
+ console.log(`Applied pending migrations to '${db}'`);
156
+ }
157
+
158
+ // Check: whether the server matches the latest local migration (same planner: empty plan = matched)
159
+ async function migrateCheck(rest) {
160
+ const { positional, flags } = parseArgs(rest);
161
+ const db = positional[0];
162
+ if (!db) { console.error("marcidb migrate check: <db> name required"); process.exit(1); }
163
+
164
+ const url = (flags.url ?? "http://localhost:3000").replace(/\/+$/, "");
165
+ const dir = flags.migrations ?? "migrations";
122
166
 
123
- if (!res.ok) {
124
- console.error(`Migration failed [${res.status}]: ${await res.text()}`);
167
+ const serverSnap = await fetchServerSnapshot(url, db);
168
+ const plan = planActions(dir, serverSnap);
169
+ if (plan.error !== undefined) {
170
+ console.error(`'${db}' has DIVERGED — the server schema isn't a point in your migration history.`);
171
+ process.exit(1);
172
+ }
173
+ if (!plan.out.trim()) {
174
+ console.log(`'${db}' matches your latest migration ✓`);
175
+ } else {
176
+ console.error(`'${db}' is behind — there are pending migrations (run "marcidb migrate push ${db}").`);
125
177
  process.exit(1);
126
178
  }
127
- console.log(`Migrated '${db}' on ${url}`);
128
179
  }
129
180
 
130
181
  const COMMANDS = {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "marcidb-client",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "license": "MIT",
6
6
  "author": {
7
7
  "name": "den59k"
package/runtime/index.js CHANGED
@@ -13,7 +13,19 @@ async function request(method, url, body) {
13
13
  }
14
14
  function encodeId(id) {
15
15
  if (typeof id === "object" && id !== null) {
16
- return Object.entries(id).map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
16
+ const parts = [];
17
+ const walk = (obj, prefix) => {
18
+ for (const [k, v] of Object.entries(obj)) {
19
+ const key = prefix ? `${prefix}.${k}` : k;
20
+ if (typeof v === "object" && v !== null) {
21
+ walk(v, key);
22
+ } else {
23
+ parts.push(`${key}=${encodeURIComponent(String(v))}`);
24
+ }
25
+ }
26
+ };
27
+ walk(id, "");
28
+ return parts.join("&");
17
29
  }
18
30
  return String(id);
19
31
  }