create-baresync 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.
- package/dist/cli.js +2531 -0
- package/dist/index.js +2529 -0
- package/dist/templates/app/db-helper.ts +22 -0
- package/dist/templates/app/drizzle-local-config.ts +19 -0
- package/dist/templates/app/package.json +17 -0
- package/dist/templates/app/src/lib.rs +21 -0
- package/dist/templates/app/src-tauri/Cargo.toml +19 -0
- package/dist/templates/app/src-tauri/build.rs +3 -0
- package/dist/templates/app/src-tauri/tauri.conf.json +27 -0
- package/dist/templates/app/sync-client.ts +11 -0
- package/dist/templates/root/README.md +11 -0
- package/dist/templates/root/package.json +14 -0
- package/dist/templates/root/scripts/dev.mjs +52 -0
- package/dist/templates/root/scripts/run-workspace.mjs +19 -0
- package/dist/templates/server/db/client.ts +32 -0
- package/dist/templates/server/db/v1/sync-repository.ts +140 -0
- package/dist/templates/server/drizzle-config.ts +15 -0
- package/dist/templates/server/fallback-instructions.md +10 -0
- package/dist/templates/server/package.json +17 -0
- package/dist/templates/server/src/index-elysia.ts +12 -0
- package/dist/templates/server/src/index-hono.ts +13 -0
- package/dist/templates/server/src/sync-route.ts +46 -0
- package/dist/templates/server/src/sync-routes.ts +46 -0
- package/dist/templates/server/src/v1/routes-elysia.ts +62 -0
- package/dist/templates/server/src/v1/routes-hono.ts +65 -0
- package/dist/templates/server/src/v1/routes.ts +76 -0
- package/dist/templates/server/src-db/client.ts +17 -0
- package/dist/templates/server/src-db/v1/sync-repository.ts +140 -0
- package/dist/templates/sync-contract/generate.ts +7 -0
- package/dist/templates/sync-contract/package.json +25 -0
- package/dist/templates/sync-contract/src/api-schema.ts +3 -0
- package/dist/templates/sync-contract/src/api-synced-schema.ts +20 -0
- package/dist/templates/sync-contract/src/constants.ts +4 -0
- package/dist/templates/sync-contract/src/index.ts +5 -0
- package/dist/templates/sync-contract/src/local-schema.ts +4 -0
- package/dist/templates/sync-contract/src/local-synced-schema.ts +22 -0
- package/dist/templates/sync-contract/sync.config.ts +17 -0
- package/dist/templates/sync-contract/tsconfig.json +17 -0
- package/package.json +35 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createTauriDrizzleDatabase } from "baresync/db";
|
|
2
|
+
import { lists, todos } from "../../../packages/sync-contract/src/local-synced-schema";
|
|
3
|
+
import {
|
|
4
|
+
syncCursors,
|
|
5
|
+
syncOutbox,
|
|
6
|
+
} from "../../../packages/sync-contract/src/local-schema";
|
|
7
|
+
|
|
8
|
+
export const TABLE = {
|
|
9
|
+
lists,
|
|
10
|
+
todos,
|
|
11
|
+
syncCursors,
|
|
12
|
+
syncOutbox,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createAppDatabase(
|
|
16
|
+
invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>
|
|
17
|
+
) {
|
|
18
|
+
return createTauriDrizzleDatabase({
|
|
19
|
+
invoke,
|
|
20
|
+
schema: TABLE,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Config } from "drizzle-kit";
|
|
2
|
+
import {
|
|
3
|
+
syncCursors,
|
|
4
|
+
syncOutbox,
|
|
5
|
+
} from "../../packages/sync-contract/src/local-schema";
|
|
6
|
+
|
|
7
|
+
const schema = {
|
|
8
|
+
syncCursors,
|
|
9
|
+
syncOutbox,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
dialect: "sqlite",
|
|
14
|
+
schema,
|
|
15
|
+
out: "./apps/app/src-tauri/migrations",
|
|
16
|
+
dbCredentials: {
|
|
17
|
+
url: "./apps/app/src-tauri/migrations/__PROJECT_NAME__.db",
|
|
18
|
+
},
|
|
19
|
+
} satisfies Config;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"db:generate:local": "drizzle-kit generate --config drizzle.local.config.ts",
|
|
7
|
+
"tauri:build": "tauri build",
|
|
8
|
+
"tauri:dev": "tauri dev"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"baresync": "^0.2.0",
|
|
12
|
+
"drizzle-orm": "^0.45.2"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"drizzle-kit": "0.31.4"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
use tauri_plugin_baresync::builder::Builder as BaresyncBuilder;
|
|
2
|
+
|
|
3
|
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
4
|
+
pub fn run() {
|
|
5
|
+
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
|
6
|
+
.init();
|
|
7
|
+
tauri::Builder::default()
|
|
8
|
+
.plugin(
|
|
9
|
+
BaresyncBuilder::new()
|
|
10
|
+
.api_base_url("http://127.0.0.1:3001")
|
|
11
|
+
.db_path("baresync.db")
|
|
12
|
+
.contract_json(include_str!(
|
|
13
|
+
"../../../../packages/sync-contract/generated/__CONTRACT_DATE__/sync-contract.json"
|
|
14
|
+
))
|
|
15
|
+
.migrations_path("migrations")
|
|
16
|
+
.poll_interval_secs(30)
|
|
17
|
+
.build(),
|
|
18
|
+
)
|
|
19
|
+
.run(tauri::generate_context!())
|
|
20
|
+
.expect("failed to run app");
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "__PROJECT_NAME__-app"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
build = "build.rs"
|
|
6
|
+
|
|
7
|
+
[lib]
|
|
8
|
+
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
9
|
+
|
|
10
|
+
[dependencies]
|
|
11
|
+
serde = { version = "1", features = ["derive"] }
|
|
12
|
+
tauri = { version = "2", features = [] }
|
|
13
|
+
tauri-plugin-baresync = "0.2.0"
|
|
14
|
+
env_logger = "0.11"
|
|
15
|
+
|
|
16
|
+
[build-dependencies]
|
|
17
|
+
tauri-build = { version = "2", features = [] }
|
|
18
|
+
|
|
19
|
+
[workspace]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://schema.tauri.app/config/2",
|
|
3
|
+
"productName": "__PROJECT_NAME__",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"identifier": "com.baresync.__PROJECT_NAME__",
|
|
6
|
+
"build": {
|
|
7
|
+
"beforeBuildCommand": "node ../../scripts/run-workspace.mjs packages/sync-contract generate",
|
|
8
|
+
"beforeDevCommand": "node ../../scripts/run-workspace.mjs packages/sync-contract generate",
|
|
9
|
+
"devUrl": "http://localhost:5173",
|
|
10
|
+
"frontendDist": "../dist"
|
|
11
|
+
},
|
|
12
|
+
"app": {
|
|
13
|
+
"withGlobalTauri": true,
|
|
14
|
+
"windows": [
|
|
15
|
+
{
|
|
16
|
+
"title": "__PROJECT_NAME__",
|
|
17
|
+
"width": 1280,
|
|
18
|
+
"height": 920,
|
|
19
|
+
"resizable": true
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"bundle": {
|
|
24
|
+
"active": false,
|
|
25
|
+
"resources": ["migrations/*.sql"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SYNC_SCOPE } from "@sync-contract/constants";
|
|
2
|
+
import { createSyncClient } from "baresync/tauri";
|
|
3
|
+
|
|
4
|
+
export function createAppSyncClient(
|
|
5
|
+
invoke: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>
|
|
6
|
+
) {
|
|
7
|
+
return createSyncClient({
|
|
8
|
+
scopeId: SYNC_SCOPE,
|
|
9
|
+
invoke,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
Generated with `create-baresync`.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- `__PACKAGE_MANAGER__ run install`
|
|
8
|
+
- `__PACKAGE_MANAGER__ run generate:sync`
|
|
9
|
+
- `__PACKAGE_MANAGER__ run migrate:local`
|
|
10
|
+
- `__PACKAGE_MANAGER__ run migrate:server`
|
|
11
|
+
- `__PACKAGE_MANAGER__ run dev`
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"workspaces": ["apps/*", "packages/*"],
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "__DEV_SCRIPT__",
|
|
7
|
+
"generate:sync": "__RUN_IN_WORKSPACE__ packages/sync-contract generate",
|
|
8
|
+
"migrate:local": "__RUN_IN_WORKSPACE__ apps/app db:generate:local",
|
|
9
|
+
"migrate:server": "__RUN_IN_WORKSPACE__ apps/server db:generate"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"concurrently": "^9.1.2"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const root = process.cwd();
|
|
5
|
+
const commands = [
|
|
6
|
+
{
|
|
7
|
+
name: "server",
|
|
8
|
+
cwd: path.join(root, "apps/server"),
|
|
9
|
+
args: ["run", "dev"],
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "app",
|
|
13
|
+
cwd: path.join(root, "apps/app"),
|
|
14
|
+
args: ["run", "tauri:dev"],
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const children = commands.map((command) =>
|
|
19
|
+
spawn("__PACKAGE_MANAGER__", command.args, {
|
|
20
|
+
cwd: command.cwd,
|
|
21
|
+
shell: true,
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
let finished = false;
|
|
27
|
+
|
|
28
|
+
function finish(code) {
|
|
29
|
+
if (finished) return;
|
|
30
|
+
finished = true;
|
|
31
|
+
|
|
32
|
+
for (const child of children) {
|
|
33
|
+
if (!child.killed) {
|
|
34
|
+
child.kill("SIGTERM");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
process.exit(code);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const child of children) {
|
|
42
|
+
child.on("close", (code) => {
|
|
43
|
+
if (code && code !== 0) {
|
|
44
|
+
finish(code);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (children.every((item) => item.exitCode !== null || item.signalCode !== null)) {
|
|
49
|
+
finish(0);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const [workspace, script, ...args] = process.argv.slice(2);
|
|
5
|
+
|
|
6
|
+
if (!workspace || !script) {
|
|
7
|
+
console.error("Usage: node ./scripts/run-workspace.mjs <workspace> <script> [args...]");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const child = spawn("__PACKAGE_MANAGER__", ["run", script, ...args], {
|
|
12
|
+
cwd: path.join(process.cwd(), workspace),
|
|
13
|
+
shell: true,
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.on("close", (code) => {
|
|
18
|
+
process.exit(code ?? 1);
|
|
19
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
6
|
+
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DB_PATH = "./data/__PROJECT_NAME__-server.db";
|
|
9
|
+
|
|
10
|
+
export interface __ProjectName__DatabaseHandle {
|
|
11
|
+
db: BunSQLiteDatabase<Record<string, never>>;
|
|
12
|
+
dbPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function create__ProjectName__Database(): Promise<__ProjectName__DatabaseHandle> {
|
|
16
|
+
const dbPath = path.resolve(
|
|
17
|
+
process.cwd(),
|
|
18
|
+
process.env.__PROJECT_NAME___SERVER_DB_PATH ?? DEFAULT_DB_PATH
|
|
19
|
+
);
|
|
20
|
+
await fs.mkdir(path.dirname(dbPath), { recursive: true });
|
|
21
|
+
|
|
22
|
+
const sqlite = new Database(dbPath);
|
|
23
|
+
sqlite.run("PRAGMA journal_mode = WAL");
|
|
24
|
+
sqlite.run("PRAGMA foreign_keys = ON");
|
|
25
|
+
|
|
26
|
+
const db = drizzle(sqlite);
|
|
27
|
+
migrate(db, {
|
|
28
|
+
migrationsFolder: path.resolve(process.cwd(), "drizzle"),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return { db, dbPath };
|
|
32
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { lists, todos } from "@sync-contract/generated/__CONTRACT_DATE__/api-synced-schema";
|
|
2
|
+
import {
|
|
3
|
+
createDrizzleSyncRepository,
|
|
4
|
+
type DrizzleSyncReadRow,
|
|
5
|
+
optionalString,
|
|
6
|
+
requiredString,
|
|
7
|
+
} from "baresync/server/drizzle";
|
|
8
|
+
import { and, desc, eq, gt, type InferInsertModel } from "drizzle-orm";
|
|
9
|
+
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
|
10
|
+
|
|
11
|
+
type AppDb = BunSQLiteDatabase<Record<string, never>>;
|
|
12
|
+
|
|
13
|
+
export interface AppScope {
|
|
14
|
+
scopeId: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createAppSyncRepository(db: AppDb) {
|
|
18
|
+
const repository = createDrizzleSyncRepository({
|
|
19
|
+
tables: {
|
|
20
|
+
lists: {
|
|
21
|
+
buildRow: ({ row, scopeId, syncUpdatedAt, updatedAt }) => ({
|
|
22
|
+
createdAt: optionalString(row.createdAt) ?? updatedAt,
|
|
23
|
+
deletedAt: optionalString(row.deletedAt),
|
|
24
|
+
id: requiredString(row.id, "lists.id"),
|
|
25
|
+
name: requiredString(row.name, "lists.name"),
|
|
26
|
+
description: optionalString(row.description),
|
|
27
|
+
scopeId,
|
|
28
|
+
syncUpdatedAt,
|
|
29
|
+
updatedAt,
|
|
30
|
+
}),
|
|
31
|
+
readLatestRow: async ({ scopeId }) => {
|
|
32
|
+
const rows = await db
|
|
33
|
+
.select()
|
|
34
|
+
.from(lists)
|
|
35
|
+
.where(eq(lists.scopeId, scopeId))
|
|
36
|
+
.orderBy(
|
|
37
|
+
desc(lists.syncUpdatedAt),
|
|
38
|
+
desc(lists.updatedAt),
|
|
39
|
+
desc(lists.id)
|
|
40
|
+
)
|
|
41
|
+
.limit(1);
|
|
42
|
+
return rows[0] ?? null;
|
|
43
|
+
},
|
|
44
|
+
readRows: ({ cursorTimestamp, scopeId }) =>
|
|
45
|
+
db
|
|
46
|
+
.select()
|
|
47
|
+
.from(lists)
|
|
48
|
+
.where(
|
|
49
|
+
cursorTimestamp > 0
|
|
50
|
+
? and(
|
|
51
|
+
eq(lists.scopeId, scopeId),
|
|
52
|
+
gt(lists.syncUpdatedAt, cursorTimestamp)
|
|
53
|
+
)
|
|
54
|
+
: eq(lists.scopeId, scopeId)
|
|
55
|
+
)
|
|
56
|
+
.orderBy(
|
|
57
|
+
desc(lists.syncUpdatedAt),
|
|
58
|
+
desc(lists.updatedAt),
|
|
59
|
+
desc(lists.id)
|
|
60
|
+
) as Promise<readonly DrizzleSyncReadRow[]>,
|
|
61
|
+
softDeleteRow: async ({ id, syncUpdatedAt, updatedAt }) => {
|
|
62
|
+
await db
|
|
63
|
+
.update(lists)
|
|
64
|
+
.set({ deletedAt: updatedAt, syncUpdatedAt, updatedAt })
|
|
65
|
+
.where(eq(lists.id, id));
|
|
66
|
+
},
|
|
67
|
+
upsertRow: async (row: InferInsertModel<typeof lists>) => {
|
|
68
|
+
const { id: _id, ...setValues } = row;
|
|
69
|
+
await db.insert(lists).values(row).onConflictDoUpdate({
|
|
70
|
+
target: lists.id,
|
|
71
|
+
set: setValues,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
todos: {
|
|
76
|
+
buildRow: ({ row, scopeId, syncUpdatedAt, updatedAt }) => ({
|
|
77
|
+
createdAt: optionalString(row.createdAt) ?? updatedAt,
|
|
78
|
+
deletedAt: optionalString(row.deletedAt),
|
|
79
|
+
id: requiredString(row.id, "todos.id"),
|
|
80
|
+
listId: requiredString(row.listId, "todos.listId"),
|
|
81
|
+
title: requiredString(row.title, "todos.title"),
|
|
82
|
+
notes: optionalString(row.notes),
|
|
83
|
+
scopeId,
|
|
84
|
+
syncUpdatedAt,
|
|
85
|
+
updatedAt,
|
|
86
|
+
}),
|
|
87
|
+
readLatestRow: async ({ scopeId }) => {
|
|
88
|
+
const rows = await db
|
|
89
|
+
.select()
|
|
90
|
+
.from(todos)
|
|
91
|
+
.where(eq(todos.scopeId, scopeId))
|
|
92
|
+
.orderBy(
|
|
93
|
+
desc(todos.syncUpdatedAt),
|
|
94
|
+
desc(todos.updatedAt),
|
|
95
|
+
desc(todos.id)
|
|
96
|
+
)
|
|
97
|
+
.limit(1);
|
|
98
|
+
return rows[0] ?? null;
|
|
99
|
+
},
|
|
100
|
+
readRows: ({ cursorTimestamp, scopeId }) =>
|
|
101
|
+
db
|
|
102
|
+
.select()
|
|
103
|
+
.from(todos)
|
|
104
|
+
.where(
|
|
105
|
+
cursorTimestamp > 0
|
|
106
|
+
? and(
|
|
107
|
+
eq(todos.scopeId, scopeId),
|
|
108
|
+
gt(todos.syncUpdatedAt, cursorTimestamp)
|
|
109
|
+
)
|
|
110
|
+
: eq(todos.scopeId, scopeId)
|
|
111
|
+
)
|
|
112
|
+
.orderBy(
|
|
113
|
+
desc(todos.syncUpdatedAt),
|
|
114
|
+
desc(todos.updatedAt),
|
|
115
|
+
desc(todos.id)
|
|
116
|
+
) as Promise<readonly DrizzleSyncReadRow[]>,
|
|
117
|
+
softDeleteRow: async ({ id, syncUpdatedAt, updatedAt }) => {
|
|
118
|
+
await db
|
|
119
|
+
.update(todos)
|
|
120
|
+
.set({ deletedAt: updatedAt, syncUpdatedAt, updatedAt })
|
|
121
|
+
.where(eq(todos.id, id));
|
|
122
|
+
},
|
|
123
|
+
upsertRow: async (row: InferInsertModel<typeof todos>) => {
|
|
124
|
+
const { id: _id, ...setValues } = row;
|
|
125
|
+
await db.insert(todos).values(row).onConflictDoUpdate({
|
|
126
|
+
target: todos.id,
|
|
127
|
+
set: setValues,
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
tableNames: repository.tableNames,
|
|
136
|
+
applyPushChanges: repository.applyPushChanges,
|
|
137
|
+
loadPullChanges: repository.loadPullChanges,
|
|
138
|
+
loadSyncStatus: repository.loadSyncStatus,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
import { syncBatchRequests } from "../../packages/sync-contract/src/api-schema";
|
|
3
|
+
|
|
4
|
+
const schema = {
|
|
5
|
+
syncBatchRequests,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
dialect: "sqlite",
|
|
10
|
+
schema,
|
|
11
|
+
out: "./drizzle",
|
|
12
|
+
dbCredentials: {
|
|
13
|
+
url: process.env.__PROJECT_NAME___SERVER_DB_PATH ?? "./data/__PROJECT_NAME__-server.db",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Manual mount required
|
|
2
|
+
|
|
3
|
+
The generated sync route module was written to `src/v1/routes.ts`.
|
|
4
|
+
|
|
5
|
+
If the entrypoint shape differs from the expected CLI template, mount the routes manually:
|
|
6
|
+
|
|
7
|
+
- Hono: `app.route("/api/v1/sync", sync)` where `sync` is the default export from `./v1/routes`
|
|
8
|
+
- Elysia: `app.use(sync)` where `sync` is exported from `./v1/routes`
|
|
9
|
+
|
|
10
|
+
Both route modules are self-contained — they create their own database connection and resolveScope. The database client is at `src/db/client.ts` and the sync repository is at `src/db/v1/sync-repository.ts`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__-server",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"db:generate": "drizzle-kit generate --config drizzle.config.ts",
|
|
7
|
+
"db:migrate": "drizzle-kit migrate --config drizzle.config.ts",
|
|
8
|
+
"predev": "drizzle-kit migrate --config drizzle.config.ts",
|
|
9
|
+
"typecheck": "bun x tsc -p tsconfig.json --noEmit"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"baresync": "^0.2.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"drizzle-kit": "0.31.4"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Elysia } from "elysia";
|
|
2
|
+
import { sync } from "./v1/routes";
|
|
3
|
+
|
|
4
|
+
const app = new Elysia()
|
|
5
|
+
.get("/health", () => ({ ok: true }))
|
|
6
|
+
.use(sync);
|
|
7
|
+
|
|
8
|
+
app.listen(Number(process.env.PORT ?? "3001"));
|
|
9
|
+
|
|
10
|
+
console.log(
|
|
11
|
+
`__PROJECT_NAME__ server listening on http://127.0.0.1:${app.server!.port}`
|
|
12
|
+
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import sync from "./v1/routes";
|
|
3
|
+
|
|
4
|
+
const app = new Hono();
|
|
5
|
+
|
|
6
|
+
app.get("/health", (c) => c.json({ ok: true }));
|
|
7
|
+
app.route("/api/v1/sync", sync);
|
|
8
|
+
|
|
9
|
+
export default app;
|
|
10
|
+
|
|
11
|
+
console.log(
|
|
12
|
+
`__PROJECT_NAME__ server listening on http://127.0.0.1:${process.env.PORT ?? "3001"}`
|
|
13
|
+
);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createSyncPullHandler, createSyncPushHandler, createSyncStatusHandler } from "baresync/server";
|
|
2
|
+
|
|
3
|
+
export function createBaresyncRoutes(deps: {
|
|
4
|
+
resolveScope: (input: { scopeId: string }) => { ok: true; scope: { scopeId: string } } | { ok: false; body: { error: string }; status: number };
|
|
5
|
+
repository: {
|
|
6
|
+
applyPushChanges: (input: { changes: unknown[]; scopeId: string; syncUpdatedAt: number }) => Promise<unknown>;
|
|
7
|
+
loadPullChanges: (input: { cursor: string; scopeId: string; tables: string[] }) => Promise<unknown>;
|
|
8
|
+
loadSyncStatus: (input: { cursor: string; scopeId: string }) => Promise<unknown>;
|
|
9
|
+
};
|
|
10
|
+
upsertOrder: string[];
|
|
11
|
+
}) {
|
|
12
|
+
const push = createSyncPushHandler({
|
|
13
|
+
encoding: "json",
|
|
14
|
+
resolveScope: deps.resolveScope,
|
|
15
|
+
upsertOrder: deps.upsertOrder,
|
|
16
|
+
applyPushChanges: async ({ changes, scope, syncUpdatedAt }) =>
|
|
17
|
+
deps.repository.applyPushChanges({
|
|
18
|
+
changes,
|
|
19
|
+
scopeId: scope.scopeId,
|
|
20
|
+
syncUpdatedAt,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const pull = createSyncPullHandler({
|
|
25
|
+
encoding: "json",
|
|
26
|
+
resolveScope: deps.resolveScope,
|
|
27
|
+
loadPullChanges: async ({ cursor, scope, tables }) =>
|
|
28
|
+
deps.repository.loadPullChanges({
|
|
29
|
+
cursor,
|
|
30
|
+
scopeId: scope.scopeId,
|
|
31
|
+
tables,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const status = createSyncStatusHandler({
|
|
36
|
+
encoding: "json",
|
|
37
|
+
resolveScope: deps.resolveScope,
|
|
38
|
+
loadSyncStatus: async ({ cursor, scope }) =>
|
|
39
|
+
deps.repository.loadSyncStatus({
|
|
40
|
+
cursor,
|
|
41
|
+
scopeId: scope.scopeId,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return { pull, push, status };
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createSyncPullHandler, createSyncPushHandler, createSyncStatusHandler } from "baresync/server";
|
|
2
|
+
|
|
3
|
+
export function createBaresyncRoutes(deps: {
|
|
4
|
+
resolveScope: (input: { scopeId: string }) => { ok: true; scope: { scopeId: string } } | { ok: false; body: { error: string }; status: number };
|
|
5
|
+
repository: {
|
|
6
|
+
applyPushChanges: (input: { changes: unknown[]; scopeId: string; syncUpdatedAt: number }) => Promise<unknown>;
|
|
7
|
+
loadPullChanges: (input: { cursor: string; scopeId: string; tables: string[] }) => Promise<unknown>;
|
|
8
|
+
loadSyncStatus: (input: { cursor: string; scopeId: string }) => Promise<unknown>;
|
|
9
|
+
};
|
|
10
|
+
upsertOrder: string[];
|
|
11
|
+
}) {
|
|
12
|
+
const push = createSyncPushHandler({
|
|
13
|
+
encoding: "json",
|
|
14
|
+
resolveScope: deps.resolveScope,
|
|
15
|
+
upsertOrder: deps.upsertOrder,
|
|
16
|
+
applyPushChanges: async ({ changes, scope, syncUpdatedAt }) =>
|
|
17
|
+
deps.repository.applyPushChanges({
|
|
18
|
+
changes,
|
|
19
|
+
scopeId: scope.scopeId,
|
|
20
|
+
syncUpdatedAt,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const pull = createSyncPullHandler({
|
|
25
|
+
encoding: "json",
|
|
26
|
+
resolveScope: deps.resolveScope,
|
|
27
|
+
loadPullChanges: async ({ cursor, scope, tables }) =>
|
|
28
|
+
deps.repository.loadPullChanges({
|
|
29
|
+
cursor,
|
|
30
|
+
scopeId: scope.scopeId,
|
|
31
|
+
tables,
|
|
32
|
+
}),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const status = createSyncStatusHandler({
|
|
36
|
+
encoding: "json",
|
|
37
|
+
resolveScope: deps.resolveScope,
|
|
38
|
+
loadSyncStatus: async ({ cursor, scope }) =>
|
|
39
|
+
deps.repository.loadSyncStatus({
|
|
40
|
+
cursor,
|
|
41
|
+
scopeId: scope.scopeId,
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return { pull, push, status };
|
|
46
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSyncPullHandler,
|
|
3
|
+
createSyncPushHandler,
|
|
4
|
+
createSyncStatusHandler,
|
|
5
|
+
} from "baresync/server";
|
|
6
|
+
import { Elysia } from "elysia";
|
|
7
|
+
import { SYNC_SCOPE } from "@sync-contract/constants";
|
|
8
|
+
import { db } from "../db/client";
|
|
9
|
+
import { createAppSyncRepository } from "../db/v1/sync-repository";
|
|
10
|
+
|
|
11
|
+
const resolveScope = ({ scopeId }: { scopeId: string }) => {
|
|
12
|
+
if (scopeId !== SYNC_SCOPE) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false as const,
|
|
15
|
+
status: 403,
|
|
16
|
+
body: { error: "single_scope_only" },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
ok: true as const,
|
|
22
|
+
scope: { scopeId },
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const repository = createAppSyncRepository(db);
|
|
27
|
+
|
|
28
|
+
const push = createSyncPushHandler({
|
|
29
|
+
resolveScope,
|
|
30
|
+
upsertOrder: repository.tableNames,
|
|
31
|
+
applyPushChanges: async ({ changes, scope, syncUpdatedAt }) =>
|
|
32
|
+
repository.applyPushChanges({
|
|
33
|
+
changes,
|
|
34
|
+
scopeId: scope.scopeId,
|
|
35
|
+
syncUpdatedAt,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const pull = createSyncPullHandler({
|
|
40
|
+
limit: 1000,
|
|
41
|
+
resolveScope,
|
|
42
|
+
loadPullChanges: async ({ cursor, scope, tables }) =>
|
|
43
|
+
repository.loadPullChanges({
|
|
44
|
+
cursor,
|
|
45
|
+
scopeId: scope.scopeId,
|
|
46
|
+
tables,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const status = createSyncStatusHandler({
|
|
51
|
+
resolveScope,
|
|
52
|
+
loadSyncStatus: async ({ cursor, scope }) =>
|
|
53
|
+
repository.loadSyncStatus({
|
|
54
|
+
cursor,
|
|
55
|
+
scopeId: scope.scopeId,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const sync = new Elysia({ prefix: "/api/v1/sync" })
|
|
60
|
+
.post("/push", async ({ request }) => push(request, {}))
|
|
61
|
+
.post("/pull", async ({ request }) => pull(request, {}))
|
|
62
|
+
.post("/status", async ({ request }) => status(request, {}));
|