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.
Files changed (39) hide show
  1. package/dist/cli.js +2531 -0
  2. package/dist/index.js +2529 -0
  3. package/dist/templates/app/db-helper.ts +22 -0
  4. package/dist/templates/app/drizzle-local-config.ts +19 -0
  5. package/dist/templates/app/package.json +17 -0
  6. package/dist/templates/app/src/lib.rs +21 -0
  7. package/dist/templates/app/src-tauri/Cargo.toml +19 -0
  8. package/dist/templates/app/src-tauri/build.rs +3 -0
  9. package/dist/templates/app/src-tauri/tauri.conf.json +27 -0
  10. package/dist/templates/app/sync-client.ts +11 -0
  11. package/dist/templates/root/README.md +11 -0
  12. package/dist/templates/root/package.json +14 -0
  13. package/dist/templates/root/scripts/dev.mjs +52 -0
  14. package/dist/templates/root/scripts/run-workspace.mjs +19 -0
  15. package/dist/templates/server/db/client.ts +32 -0
  16. package/dist/templates/server/db/v1/sync-repository.ts +140 -0
  17. package/dist/templates/server/drizzle-config.ts +15 -0
  18. package/dist/templates/server/fallback-instructions.md +10 -0
  19. package/dist/templates/server/package.json +17 -0
  20. package/dist/templates/server/src/index-elysia.ts +12 -0
  21. package/dist/templates/server/src/index-hono.ts +13 -0
  22. package/dist/templates/server/src/sync-route.ts +46 -0
  23. package/dist/templates/server/src/sync-routes.ts +46 -0
  24. package/dist/templates/server/src/v1/routes-elysia.ts +62 -0
  25. package/dist/templates/server/src/v1/routes-hono.ts +65 -0
  26. package/dist/templates/server/src/v1/routes.ts +76 -0
  27. package/dist/templates/server/src-db/client.ts +17 -0
  28. package/dist/templates/server/src-db/v1/sync-repository.ts +140 -0
  29. package/dist/templates/sync-contract/generate.ts +7 -0
  30. package/dist/templates/sync-contract/package.json +25 -0
  31. package/dist/templates/sync-contract/src/api-schema.ts +3 -0
  32. package/dist/templates/sync-contract/src/api-synced-schema.ts +20 -0
  33. package/dist/templates/sync-contract/src/constants.ts +4 -0
  34. package/dist/templates/sync-contract/src/index.ts +5 -0
  35. package/dist/templates/sync-contract/src/local-schema.ts +4 -0
  36. package/dist/templates/sync-contract/src/local-synced-schema.ts +22 -0
  37. package/dist/templates/sync-contract/sync.config.ts +17 -0
  38. package/dist/templates/sync-contract/tsconfig.json +17 -0
  39. package/package.json +35 -0
@@ -0,0 +1,65 @@
1
+ import { Hono } from "hono";
2
+ import { SYNC_SCOPE } from "@sync-contract/constants";
3
+ import {
4
+ createSyncPullHandler,
5
+ createSyncPushHandler,
6
+ createSyncStatusHandler,
7
+ } from "baresync/server";
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
+ const sync = new Hono();
60
+
61
+ sync.post("/push", (c) => push(c.req.raw, {}));
62
+ sync.post("/pull", (c) => pull(c.req.raw, {}));
63
+ sync.post("/status", (c) => status(c.req.raw, {}));
64
+
65
+ export default sync;
@@ -0,0 +1,76 @@
1
+ import { Hono } from "hono";
2
+ import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
3
+ import {
4
+ createSyncPullHandler,
5
+ createSyncPushHandler,
6
+ createSyncStatusHandler,
7
+ } from "baresync/server";
8
+ import {
9
+ createAppSyncRepository,
10
+ type AppScope,
11
+ } from "../db/v1/sync-repository";
12
+
13
+ type AppDb = BunSQLiteDatabase<Record<string, never>>;
14
+
15
+ export function createV1Routes({
16
+ db,
17
+ resolveScope,
18
+ }: {
19
+ db: AppDb;
20
+ resolveScope: ({
21
+ scopeId,
22
+ }: {
23
+ scopeId: string;
24
+ }) =>
25
+ | { ok: true; scope: AppScope }
26
+ | { ok: false; status: number; body: { error: string } };
27
+ }) {
28
+ const repository = createAppSyncRepository(db);
29
+
30
+ const app = new Hono();
31
+
32
+ app.post("/push", async (c) => {
33
+ const handler = createSyncPushHandler({
34
+ encoding: "json",
35
+ resolveScope,
36
+ upsertOrder: repository.tableNames,
37
+ applyPushChanges: async ({ changes, scope, syncUpdatedAt }) =>
38
+ repository.applyPushChanges({
39
+ changes,
40
+ scopeId: scope.scopeId,
41
+ syncUpdatedAt,
42
+ }),
43
+ });
44
+ return handler(c.req.raw, {});
45
+ });
46
+
47
+ app.post("/pull", async (c) => {
48
+ const handler = createSyncPullHandler({
49
+ encoding: "json",
50
+ limit: 1000,
51
+ resolveScope,
52
+ loadPullChanges: async ({ cursor, scope, tables }) =>
53
+ repository.loadPullChanges({
54
+ cursor,
55
+ scopeId: scope.scopeId,
56
+ tables,
57
+ }),
58
+ });
59
+ return handler(c.req.raw, {});
60
+ });
61
+
62
+ app.post("/status", async (c) => {
63
+ const handler = createSyncStatusHandler({
64
+ encoding: "json",
65
+ resolveScope,
66
+ loadSyncStatus: async ({ cursor, scope }) =>
67
+ repository.loadSyncStatus({
68
+ cursor,
69
+ scopeId: scope.scopeId,
70
+ }),
71
+ });
72
+ return handler(c.req.raw, {});
73
+ });
74
+
75
+ return app;
76
+ }
@@ -0,0 +1,17 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { drizzle } from "drizzle-orm/better-sqlite3";
5
+
6
+ const dbPath = path.resolve(
7
+ process.cwd(),
8
+ process.env.__PROJECT_NAME___SERVER_DB_PATH ?? "./data/__PROJECT_NAME__-server.db"
9
+ );
10
+
11
+ await fs.mkdir(path.dirname(dbPath), { recursive: true });
12
+
13
+ const sqlite = new Database(dbPath);
14
+ sqlite.pragma("journal_mode = WAL");
15
+ sqlite.pragma("foreign_keys = ON");
16
+
17
+ export const db = drizzle(sqlite);
@@ -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 { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
10
+
11
+ type AppDb = BetterSQLite3Database<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,7 @@
1
+ import { generateSyncArtifacts } from "baresync/generator";
2
+ import { syncGeneratorConfig } from "./sync.config";
3
+
4
+ generateSyncArtifacts(
5
+ syncGeneratorConfig.contract,
6
+ syncGeneratorConfig.outputDir
7
+ );
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@baresync/sync-contract",
3
+ "private": true,
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/constants.ts",
7
+ "./constants": "./src/constants.ts",
8
+ "./api-schema": "./src/api-schema.ts",
9
+ "./api-synced-schema": "./src/api-synced-schema.ts",
10
+ "./local-schema": "./src/local-schema.ts",
11
+ "./local-synced-schema": "./src/local-synced-schema.ts",
12
+ "./generated/*": "./generated/*"
13
+ },
14
+ "scripts": {
15
+ "generate": "bunx baresync generate",
16
+ "typecheck": "bun x tsc -p tsconfig.json --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "baresync": "^0.2.0",
20
+ "drizzle-orm": "^0.45.2"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^24.0.10"
24
+ }
25
+ }
@@ -0,0 +1,3 @@
1
+ import { createSyncBatchRequestsTable } from "baresync/schema";
2
+
3
+ export const syncBatchRequests = createSyncBatchRequestsTable();
@@ -0,0 +1,20 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+ import { apiSyncColumns } from "baresync/schema";
3
+
4
+ export const lists = sqliteTable("lists", {
5
+ id: text("id").primaryKey(),
6
+ scopeId: text("scope_id").notNull(),
7
+ name: text("name").notNull(),
8
+ description: text("description"),
9
+ ...apiSyncColumns(),
10
+ });
11
+
12
+ export const todos = sqliteTable("todos", {
13
+ id: text("id").primaryKey(),
14
+ scopeId: text("scope_id").notNull(),
15
+ listId: text("list_id").notNull(),
16
+ title: text("title").notNull(),
17
+ completed: integer("completed", { mode: "boolean" }).notNull().default(false),
18
+ notes: text("notes"),
19
+ ...apiSyncColumns(),
20
+ });
@@ -0,0 +1,4 @@
1
+ export const PROJECT_NAME = "__PROJECT_NAME__";
2
+ export const SYNC_SCOPE = "default";
3
+ export const LISTS_TABLE_NAME = "lists";
4
+ export const TODOS_TABLE_NAME = "todos";
@@ -0,0 +1,5 @@
1
+ export * from "./api-schema";
2
+ export * from "./api-synced-schema";
3
+ export * from "./constants";
4
+ export * from "./local-schema";
5
+ export * from "./local-synced-schema";
@@ -0,0 +1,4 @@
1
+ import { createSyncCursorsTable, createSyncOutboxTable } from "baresync/schema";
2
+
3
+ export const syncOutbox = createSyncOutboxTable();
4
+ export const syncCursors = createSyncCursorsTable();
@@ -0,0 +1,22 @@
1
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+ import { localSyncColumns } from "baresync/schema";
3
+
4
+ export const lists = sqliteTable("lists", {
5
+ id: text("id").primaryKey(),
6
+ scopeId: text("scope_id").notNull(),
7
+ name: text("name").notNull(),
8
+ description: text("description"),
9
+ ...localSyncColumns(),
10
+ });
11
+
12
+ export const todos = sqliteTable("todos", {
13
+ id: text("id").primaryKey(),
14
+ scopeId: text("scope_id").notNull(),
15
+ listId: text("list_id")
16
+ .notNull()
17
+ .references(() => lists.id, { onDelete: "cascade" }),
18
+ title: text("title").notNull(),
19
+ completed: integer("completed", { mode: "boolean" }).notNull().default(false),
20
+ notes: text("notes"),
21
+ ...localSyncColumns(),
22
+ });
@@ -0,0 +1,17 @@
1
+ import { defineSyncConfig } from "baresync/generator";
2
+ import * as apiSyncedSchema from "./src/api-synced-schema";
3
+ import * as localSyncedSchema from "./src/local-synced-schema";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
7
+
8
+ export const syncGeneratorConfig = defineSyncConfig({
9
+ apiSyncedSchema,
10
+ localSyncedSchema,
11
+ outputDir: "./generated",
12
+ schemaSourceDir: `${__dirname}src`,
13
+ tables: {
14
+ lists: { scopeColumn: "scope_id" },
15
+ todos: { scopeColumn: "scope_id" },
16
+ },
17
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "declaration": true,
11
+ "sourceMap": true,
12
+ "outDir": "dist",
13
+ "rootDir": "../..",
14
+ "types": ["node"]
15
+ },
16
+ "include": ["src", "sync.config.ts"]
17
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "create-baresync",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Scaffold Baresync starter workspaces",
7
+ "license": "MIT",
8
+ "packageManager": "bun@1.3.13",
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "bin": {
13
+ "create-baresync": "./dist/cli.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "bun run src/build.ts",
21
+ "typecheck": "bun x tsc -p tsconfig.json --noEmit",
22
+ "test": "bun x vitest run",
23
+ "clean": "rm -rf dist"
24
+ },
25
+ "dependencies": {
26
+ "@clack/prompts": "1.4.0",
27
+ "jsonrepair": "^3.14.0",
28
+ "picocolors": "1.1.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^24.0.10",
32
+ "typescript": "5.9.2",
33
+ "vitest": "^4.1.6"
34
+ }
35
+ }