Skip to content

Commit 9ee3418

Browse files
feat: add Appwrite database management scripts and trial sessions setup
- Introduce `appwrite-ensure-database.ts` to ensure the Appwrite database exists or create it if missing. - Update `setup-ai-keys-appwrite.ts` and `setup-subscriptions-appwrite.ts` to utilize the new database management function. - Add `setup-trial-sessions-appwrite.ts` for setting up trial sessions, including collection and attribute creation with error handling. - Enhance package.json with a new script for setting up trial sessions.
1 parent 06d86de commit 9ee3418

5 files changed

Lines changed: 285 additions & 26 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"test:market-data": "bun run scripts/test-market-data.ts",
1717
"setup:appwrite:ai-keys": "bun run scripts/setup-ai-keys-appwrite.ts",
1818
"setup:appwrite:subscriptions": "bun run scripts/setup-subscriptions-appwrite.ts",
19+
"setup:appwrite:trial-sessions": "bun run scripts/setup-trial-sessions-appwrite.ts",
1920
"analyze": "ANALYZE=true bun run build",
2021
"lighthouse": "lhci autorun"
2122
},
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { AppwriteException, type Databases } from "node-appwrite";
2+
3+
function readEnv(name: string): string {
4+
const value = process.env[name];
5+
return typeof value === "string" ? value.trim() : "";
6+
}
7+
8+
function isNotFoundError(error: unknown): boolean {
9+
if (error instanceof AppwriteException && error.code === 404) return true;
10+
const message =
11+
error instanceof Error ? error.message.toLowerCase() : String(error);
12+
return message.includes("not found") || message.includes("404");
13+
}
14+
15+
function isAlreadyExistsError(error: unknown): boolean {
16+
const message =
17+
error instanceof Error ? error.message.toLowerCase() : String(error);
18+
return (
19+
message.includes("already exists") ||
20+
message.includes("conflict") ||
21+
message.includes("409")
22+
);
23+
}
24+
25+
/**
26+
* Ensures the Appwrite database exists (creates it if missing).
27+
* Uses APPWRITE_DATABASE_NAME when set; otherwise the database id as the label.
28+
*/
29+
export async function ensureAppwriteDatabase(
30+
databases: Databases,
31+
databaseId: string
32+
): Promise<void> {
33+
try {
34+
await databases.get(databaseId);
35+
console.log(`Using existing database: ${databaseId}`);
36+
return;
37+
} catch (error) {
38+
if (!isNotFoundError(error)) throw error;
39+
}
40+
41+
const displayName = readEnv("APPWRITE_DATABASE_NAME") || databaseId;
42+
43+
try {
44+
await databases.create(databaseId, displayName, true);
45+
console.log(`Created database: ${databaseId} ("${displayName}")`);
46+
} catch (error) {
47+
if (isAlreadyExistsError(error)) {
48+
console.log(`Database already exists: ${databaseId}`);
49+
return;
50+
}
51+
throw error;
52+
}
53+
}

scripts/setup-ai-keys-appwrite.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Client, Databases } from "node-appwrite";
2+
import { ensureAppwriteDatabase } from "./appwrite-ensure-database";
23

34
function readEnv(name: string): string {
45
const value = process.env[name];
@@ -36,19 +37,7 @@ async function main(): Promise<void> {
3637
.setKey(apiKey);
3738
const databases = new Databases(client);
3839

39-
try {
40-
await databases.get(databaseId);
41-
console.log(`Using existing database: ${databaseId}`);
42-
} catch (error) {
43-
const message =
44-
error instanceof Error ? error.message.toLowerCase() : String(error);
45-
if (message.includes("not found") || message.includes("404")) {
46-
throw new Error(
47-
`Database "${databaseId}" was not found. Set APPWRITE_DATABASE_ID to an existing Appwrite database id.`
48-
);
49-
}
50-
throw error;
51-
}
40+
await ensureAppwriteDatabase(databases, databaseId);
5241

5342
try {
5443
await databases.createCollection(

scripts/setup-subscriptions-appwrite.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { Client, Databases, IndexType } from "node-appwrite";
1+
import { AppwriteException, Client, Databases, IndexType } from "node-appwrite";
2+
import { ensureAppwriteDatabase } from "./appwrite-ensure-database";
3+
4+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
25

36
function readEnv(name: string): string {
47
const value = process.env[name];
@@ -23,6 +26,17 @@ function isAlreadyExistsError(error: unknown): boolean {
2326
);
2427
}
2528

29+
function isAttributeNotAvailableError(error: unknown): boolean {
30+
if (
31+
error instanceof AppwriteException &&
32+
error.type === "attribute_not_available"
33+
)
34+
return true;
35+
const message =
36+
error instanceof Error ? error.message.toLowerCase() : String(error);
37+
return message.includes("not yet available");
38+
}
39+
2640
async function main() {
2741
const endpoint = requireEnv("NEXT_PUBLIC_APPWRITE_ENDPOINT");
2842
const projectId = requireEnv("NEXT_PUBLIC_APPWRITE_PROJECT_ID");
@@ -36,12 +50,7 @@ async function main() {
3650
.setKey(apiKey);
3751
const databases = new Databases(client);
3852

39-
try {
40-
await databases.get(databaseId);
41-
console.log(`Using existing database: ${databaseId}`);
42-
} catch {
43-
throw new Error(`Database "${databaseId}" does not exist.`);
44-
}
53+
await ensureAppwriteDatabase(databases, databaseId);
4554

4655
try {
4756
await databases.createCollection(
@@ -142,12 +151,27 @@ async function main() {
142151
);
143152

144153
const createIndex = async (key: string, fn: () => Promise<unknown>) => {
145-
try {
146-
await fn();
147-
console.log(`Created index: ${key}`);
148-
} catch (error) {
149-
if (!isAlreadyExistsError(error)) throw error;
150-
console.log(`Index already exists: ${key}`);
154+
const maxAttempts = 36;
155+
const delayMs = 2500;
156+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
157+
try {
158+
await fn();
159+
console.log(`Created index: ${key}`);
160+
return;
161+
} catch (error) {
162+
if (isAlreadyExistsError(error)) {
163+
console.log(`Index already exists: ${key}`);
164+
return;
165+
}
166+
if (isAttributeNotAvailableError(error) && attempt < maxAttempts) {
167+
console.log(
168+
`Attributes still provisioning; retrying index "${key}" (${attempt}/${maxAttempts})...`
169+
);
170+
await sleep(delayMs);
171+
continue;
172+
}
173+
throw error;
174+
}
151175
}
152176
};
153177

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { AppwriteException, Client, Databases, IndexType } from "node-appwrite";
2+
import { ensureAppwriteDatabase } from "./appwrite-ensure-database";
3+
4+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
5+
6+
function readEnv(name: string): string {
7+
const value = process.env[name];
8+
return typeof value === "string" ? value.trim() : "";
9+
}
10+
11+
function requireEnv(name: string): string {
12+
const value = readEnv(name);
13+
if (!value) {
14+
throw new Error(`Missing required env var: ${name}`);
15+
}
16+
return value;
17+
}
18+
19+
function isAlreadyExistsError(error: unknown): boolean {
20+
const message =
21+
error instanceof Error ? error.message.toLowerCase() : String(error);
22+
return (
23+
message.includes("already exists") ||
24+
message.includes("conflict") ||
25+
message.includes("409")
26+
);
27+
}
28+
29+
function isAttributeNotAvailableError(error: unknown): boolean {
30+
if (
31+
error instanceof AppwriteException &&
32+
error.type === "attribute_not_available"
33+
)
34+
return true;
35+
const message =
36+
error instanceof Error ? error.message.toLowerCase() : String(error);
37+
return message.includes("not yet available");
38+
}
39+
40+
async function main(): Promise<void> {
41+
const endpoint = requireEnv("NEXT_PUBLIC_APPWRITE_ENDPOINT");
42+
const projectId = requireEnv("NEXT_PUBLIC_APPWRITE_PROJECT_ID");
43+
const apiKey = requireEnv("APPWRITE_API_KEY");
44+
const databaseId = requireEnv("APPWRITE_DATABASE_ID");
45+
const collectionId = requireEnv("APPWRITE_COLLECTION_ID_TRIAL_SESSIONS");
46+
47+
const client = new Client()
48+
.setEndpoint(endpoint)
49+
.setProject(projectId)
50+
.setKey(apiKey);
51+
const databases = new Databases(client);
52+
53+
await ensureAppwriteDatabase(databases, databaseId);
54+
55+
try {
56+
await databases.createCollection(
57+
databaseId,
58+
collectionId,
59+
"Trial sessions",
60+
[],
61+
true,
62+
true
63+
);
64+
console.log(`Created collection: ${collectionId}`);
65+
} catch (error) {
66+
if (!isAlreadyExistsError(error)) throw error;
67+
console.log(`Collection already exists: ${collectionId}`);
68+
}
69+
70+
const createAttribute = async (label: string, fn: () => Promise<unknown>) => {
71+
try {
72+
await fn();
73+
console.log(`Created attribute: ${label}`);
74+
} catch (error) {
75+
if (!isAlreadyExistsError(error)) throw error;
76+
console.log(`Attribute already exists: ${label}`);
77+
}
78+
};
79+
80+
await createAttribute("fingerprint", () =>
81+
databases.createStringAttribute(
82+
databaseId,
83+
collectionId,
84+
"fingerprint",
85+
512,
86+
true
87+
)
88+
);
89+
await createAttribute("ipAddress", () =>
90+
databases.createStringAttribute(
91+
databaseId,
92+
collectionId,
93+
"ipAddress",
94+
128,
95+
false,
96+
""
97+
)
98+
);
99+
await createAttribute("startTime", () =>
100+
databases.createDatetimeAttribute(
101+
databaseId,
102+
collectionId,
103+
"startTime",
104+
true
105+
)
106+
);
107+
await createAttribute("endTime", () =>
108+
databases.createDatetimeAttribute(databaseId, collectionId, "endTime", true)
109+
);
110+
await createAttribute("isActive", () =>
111+
databases.createBooleanAttribute(databaseId, collectionId, "isActive", true)
112+
);
113+
await createAttribute("userAgent", () =>
114+
databases.createStringAttribute(
115+
databaseId,
116+
collectionId,
117+
"userAgent",
118+
2048,
119+
true
120+
)
121+
);
122+
await createAttribute("screenResolution", () =>
123+
databases.createStringAttribute(
124+
databaseId,
125+
collectionId,
126+
"screenResolution",
127+
64,
128+
true
129+
)
130+
);
131+
await createAttribute("timezone", () =>
132+
databases.createStringAttribute(
133+
databaseId,
134+
collectionId,
135+
"timezone",
136+
128,
137+
true
138+
)
139+
);
140+
141+
const createIndex = async (key: string, fn: () => Promise<unknown>) => {
142+
const maxAttempts = 36;
143+
const delayMs = 2500;
144+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
145+
try {
146+
await fn();
147+
console.log(`Created index: ${key}`);
148+
return;
149+
} catch (error) {
150+
if (isAlreadyExistsError(error)) {
151+
console.log(`Index already exists: ${key}`);
152+
return;
153+
}
154+
if (isAttributeNotAvailableError(error) && attempt < maxAttempts) {
155+
console.log(
156+
`Attributes still provisioning; retrying index "${key}" (${attempt}/${maxAttempts})...`
157+
);
158+
await sleep(delayMs);
159+
continue;
160+
}
161+
throw error;
162+
}
163+
}
164+
};
165+
166+
await createIndex("idx_fingerprint", () =>
167+
databases.createIndex(
168+
databaseId,
169+
collectionId,
170+
"idx_fingerprint",
171+
IndexType.Key,
172+
["fingerprint"]
173+
)
174+
);
175+
await createIndex("idx_ipAddress", () =>
176+
databases.createIndex(
177+
databaseId,
178+
collectionId,
179+
"idx_ipAddress",
180+
IndexType.Key,
181+
["ipAddress"]
182+
)
183+
);
184+
185+
console.log("Trial sessions Appwrite setup complete.");
186+
}
187+
188+
main().catch((error) => {
189+
console.error("Failed to setup trial sessions collection.");
190+
console.error(error);
191+
process.exit(1);
192+
});

0 commit comments

Comments
 (0)