@stardeck-customer-apps/data-store-sdk 0.1.0-preview.0 → 0.1.0-preview.2
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/SKILL.md +294 -0
- package/dist/cli/generate-types.js +1 -1
- package/dist/cli/generate-types.mjs +1 -1
- package/dist/server/index.js +12 -6
- package/dist/server/index.mjs +12 -6
- package/package.json +3 -2
package/SKILL.md
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Data Store SDK - AI Agent Skill Guide
|
|
2
|
+
|
|
3
|
+
This guide helps AI agents understand and implement data store features using `@stardeck-customer-apps/data-store-sdk`.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
**Package**: `@stardeck-customer-apps/data-store-sdk`
|
|
8
|
+
|
|
9
|
+
**Exports**:
|
|
10
|
+
|
|
11
|
+
- Core: `@stardeck-customer-apps/data-store-sdk` - Types and error classes
|
|
12
|
+
- Server: `@stardeck-customer-apps/data-store-sdk/server` - DataStoreClient, Kysely wrapper, HMAC signing
|
|
13
|
+
|
|
14
|
+
**Two Access Patterns**:
|
|
15
|
+
|
|
16
|
+
1. **DataStoreClient** — Platform API client for schema management and dynamic queries (no generated types needed)
|
|
17
|
+
2. **createDataStore()** — Direct Kysely connection for type-safe queries (requires generated types + DATA_STORE_URL)
|
|
18
|
+
|
|
19
|
+
## Architecture Overview
|
|
20
|
+
|
|
21
|
+
**Schema Management (via Platform API):**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Server Code
|
|
25
|
+
↓ (DataStoreClient with HMAC auth)
|
|
26
|
+
Platform API (/api/data-stores/{storeId}/schema/*)
|
|
27
|
+
↓
|
|
28
|
+
Data Store Neon Database (DDL applied)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Dynamic Queries (via Platform API):**
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Server Code
|
|
35
|
+
↓ (DataStoreClient with HMAC auth)
|
|
36
|
+
Platform API (/api/data-stores/{storeId}/query or /mutate)
|
|
37
|
+
↓
|
|
38
|
+
Data Store Neon Database
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Type-Safe Queries (Direct Connection):**
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
Server Code
|
|
45
|
+
↓ (Kysely with DATA_STORE_URL)
|
|
46
|
+
Data Store Neon Database (direct connection)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
### 1. Install Dependencies
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @stardeck-customer-apps/data-store-sdk
|
|
55
|
+
# For direct Kysely access (optional):
|
|
56
|
+
npm install kysely kysely-neon @neondatabase/serverless
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Environment Variables
|
|
60
|
+
|
|
61
|
+
These are automatically injected by the platform at deploy time:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
CONTROL_PLANE_URL=https://app.stardeck.ai # Platform API URL
|
|
65
|
+
DEPLOYMENT_SECRET=your-secret-key # HMAC signing (NEVER expose to client)
|
|
66
|
+
ORGANIZATION_ID=org_123
|
|
67
|
+
PROJECT_ID=proj_456
|
|
68
|
+
DEPLOYMENT_ID=deploy_789
|
|
69
|
+
DATA_STORE_MYSTORE_URL=postgres://... # Per-store connection string (for Kysely)
|
|
70
|
+
DATA_STORE_MYSTORE_ID=ds-abc123 # Per-store ID (for DataStoreClient)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Pattern 1: DataStoreClient (Dynamic Queries)
|
|
74
|
+
|
|
75
|
+
Use this for schema operations, agent-driven data access, or when you don't have generated types.
|
|
76
|
+
|
|
77
|
+
### Initialize
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// lib/data-store.ts (server-only)
|
|
81
|
+
import { DataStoreClient } from "@stardeck-customer-apps/data-store-sdk/server";
|
|
82
|
+
|
|
83
|
+
export const dataStore = new DataStoreClient({
|
|
84
|
+
storeId: process.env.DATA_STORE_MYSTORE_ID!,
|
|
85
|
+
// Other config read from env vars automatically
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Schema Operations
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Create a table
|
|
93
|
+
await dataStore.createTable("orders", [
|
|
94
|
+
{ name: "customer_name", fieldType: "text" },
|
|
95
|
+
{ name: "total", fieldType: "currency", config: { currency: "USD" } },
|
|
96
|
+
{ name: "status", fieldType: "select", config: { options: ["pending", "shipped", "delivered"] } },
|
|
97
|
+
{ name: "notes", fieldType: "long_text", nullable: true },
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// Add a column to an existing table
|
|
101
|
+
await dataStore.addColumn("orders", {
|
|
102
|
+
name: "priority",
|
|
103
|
+
fieldType: "rating",
|
|
104
|
+
config: { max: 5 },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Read schema
|
|
108
|
+
const { tables } = await dataStore.getSchema();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Available Field Types
|
|
112
|
+
|
|
113
|
+
| Field Type | Postgres Type | Config Options |
|
|
114
|
+
| -------------- | ------------- | ------------------------------------------------- |
|
|
115
|
+
| `text` | text | — |
|
|
116
|
+
| `long_text` | text | — |
|
|
117
|
+
| `number` | numeric | — |
|
|
118
|
+
| `boolean` | boolean | — |
|
|
119
|
+
| `date` | date | — |
|
|
120
|
+
| `datetime` | timestamptz | — |
|
|
121
|
+
| `select` | text | `{ options: string[] }` |
|
|
122
|
+
| `multi_select` | text[] | `{ options: string[] }` |
|
|
123
|
+
| `currency` | numeric | `{ currency: string }` |
|
|
124
|
+
| `email` | text | — |
|
|
125
|
+
| `phone` | text | — |
|
|
126
|
+
| `url` | text | — |
|
|
127
|
+
| `rating` | integer | `{ max: number }` |
|
|
128
|
+
| `relation` | uuid | `{ targetTable: string, displayColumn?: string }` |
|
|
129
|
+
| `file_ref` | text | — |
|
|
130
|
+
| `json` | jsonb | — |
|
|
131
|
+
|
|
132
|
+
### Query Data
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
// Simple query
|
|
136
|
+
const { rows, total } = await dataStore.query("orders", {
|
|
137
|
+
filters: [
|
|
138
|
+
{ column: "status", operator: "eq", value: "pending" },
|
|
139
|
+
],
|
|
140
|
+
orderBy: "created_at",
|
|
141
|
+
orderDir: "desc",
|
|
142
|
+
limit: 20,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Available filter operators: eq, neq, gt, lt, contains, is_null, is_not_null
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Mutate Data
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Insert
|
|
152
|
+
const { row } = await dataStore.insert("orders", {
|
|
153
|
+
customer_name: "Alice",
|
|
154
|
+
total: 99.99,
|
|
155
|
+
status: "pending",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Update (by primary key)
|
|
159
|
+
await dataStore.update("orders", { id: row.id }, "status", "shipped");
|
|
160
|
+
|
|
161
|
+
// Delete (by primary key)
|
|
162
|
+
await dataStore.delete("orders", { id: row.id });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Storage Operations (for storage-type data stores)
|
|
166
|
+
|
|
167
|
+
> **Not the Storage SDK.** Storage-type data stores are org-level shared file storage accessed via `DataStoreClient` (this SDK). They are a completely separate system from the legacy Storage SDK (`@stardeck-customer-apps/storage-sdk`). Do not confuse the two:
|
|
168
|
+
>
|
|
169
|
+
> - **Storage SDK (legacy)** = project-scoped file storage for a single app. Being phased out in favor of storage-type data stores.
|
|
170
|
+
> - **Data Store storage** = org-scoped shared file storage accessed by multiple projects via `DataStoreClient`. This is the recommended approach for file storage.
|
|
171
|
+
>
|
|
172
|
+
> Always prefer data store storage over the Storage SDK for new code.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// List files
|
|
176
|
+
const { objects, folders } = await dataStore.listFiles({ prefix: "invoices/" });
|
|
177
|
+
|
|
178
|
+
// Get upload URL (client uploads directly to R2)
|
|
179
|
+
const { uploadUrl } = await dataStore.getUploadUrl("invoices/march.pdf", "application/pdf", fileSize);
|
|
180
|
+
|
|
181
|
+
// Get download URL
|
|
182
|
+
const { downloadUrl } = await dataStore.getDownloadUrl("invoices/march.pdf");
|
|
183
|
+
|
|
184
|
+
// Delete files
|
|
185
|
+
await dataStore.deleteFiles(["invoices/old.pdf"]);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Pattern 2: Kysely (Type-Safe Queries)
|
|
189
|
+
|
|
190
|
+
Use this for application code where you want compile-time type safety.
|
|
191
|
+
|
|
192
|
+
### Generate Types
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
npx stardeck-data-store generate-types --connection-string $DATA_STORE_MYSTORE_URL
|
|
196
|
+
# Outputs: ./src/generated/data-store-types.ts
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Initialize
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
// lib/db.ts (server-only)
|
|
203
|
+
import { createDataStore } from "@stardeck-customer-apps/data-store-sdk/server";
|
|
204
|
+
import type { DB } from "@/generated/data-store-types";
|
|
205
|
+
|
|
206
|
+
export const db = await createDataStore<DB>({
|
|
207
|
+
connectionString: process.env.DATA_STORE_MYSTORE_URL,
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Query with Full Type Safety
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { db } from "@/lib/db";
|
|
215
|
+
|
|
216
|
+
// Select with type inference
|
|
217
|
+
const orders = await db
|
|
218
|
+
.selectFrom("orders")
|
|
219
|
+
.where("status", "=", "pending")
|
|
220
|
+
.orderBy("created_at", "desc")
|
|
221
|
+
.selectAll()
|
|
222
|
+
.execute();
|
|
223
|
+
|
|
224
|
+
// Insert
|
|
225
|
+
const newOrder = await db
|
|
226
|
+
.insertInto("orders")
|
|
227
|
+
.values({
|
|
228
|
+
customer_name: "Bob",
|
|
229
|
+
total: 149.99,
|
|
230
|
+
status: "pending",
|
|
231
|
+
})
|
|
232
|
+
.returningAll()
|
|
233
|
+
.executeTakeFirstOrThrow();
|
|
234
|
+
|
|
235
|
+
// Update
|
|
236
|
+
await db
|
|
237
|
+
.updateTable("orders")
|
|
238
|
+
.set({ status: "shipped" })
|
|
239
|
+
.where("id", "=", orderId)
|
|
240
|
+
.execute();
|
|
241
|
+
|
|
242
|
+
// Join
|
|
243
|
+
const ordersWithCustomers = await db
|
|
244
|
+
.selectFrom("orders")
|
|
245
|
+
.innerJoin("customers", "customers.id", "orders.customer_id")
|
|
246
|
+
.select(["orders.id", "orders.total", "customers.name"])
|
|
247
|
+
.execute();
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Error Handling
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import {
|
|
254
|
+
DataStoreError,
|
|
255
|
+
AuthenticationError,
|
|
256
|
+
ForbiddenError,
|
|
257
|
+
ValidationError,
|
|
258
|
+
NotFoundError,
|
|
259
|
+
} from "@stardeck-customer-apps/data-store-sdk";
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await dataStore.query("orders");
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (error instanceof AuthenticationError) {
|
|
265
|
+
// HMAC signature invalid or expired
|
|
266
|
+
} else if (error instanceof ForbiddenError) {
|
|
267
|
+
// Insufficient access level for this operation
|
|
268
|
+
} else if (error instanceof ValidationError) {
|
|
269
|
+
// Invalid request (bad table name, invalid filter, etc.)
|
|
270
|
+
} else if (error instanceof NotFoundError) {
|
|
271
|
+
// Data store or table not found
|
|
272
|
+
} else if (error instanceof DataStoreError) {
|
|
273
|
+
// General API error — check error.code and error.statusCode
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## When to Use Which Pattern
|
|
279
|
+
|
|
280
|
+
| Scenario | Pattern |
|
|
281
|
+
| ----------------------------------------- | ------------------ |
|
|
282
|
+
| Application CRUD with known schema | Kysely (type-safe) |
|
|
283
|
+
| Agent building features dynamically | DataStoreClient |
|
|
284
|
+
| Schema management (create/alter tables) | DataStoreClient |
|
|
285
|
+
| Edge functions / serverless | DataStoreClient |
|
|
286
|
+
| Complex queries (joins, CTEs, subqueries) | Kysely |
|
|
287
|
+
| Storage file operations | DataStoreClient |
|
|
288
|
+
|
|
289
|
+
## Important Notes
|
|
290
|
+
|
|
291
|
+
- **DEPLOYMENT_SECRET must never be exposed to the client.** All data store operations must happen server-side.
|
|
292
|
+
- **Tables auto-generate** `id` (UUID), `created_at`, and `updated_at` columns. Don't include these in create table definitions.
|
|
293
|
+
- **Column deletes are soft-deletes.** The column is renamed to `_deleted_{name}_{timestamp}` and can be restored.
|
|
294
|
+
- **Type changes are restricted** to safe widening conversions (e.g., integer → bigint, boolean → text).
|
package/dist/server/index.js
CHANGED
|
@@ -90,9 +90,16 @@ var DataStoreClient = class {
|
|
|
90
90
|
this.deploymentId = config.deploymentId || this.getEnv("DEPLOYMENT_ID") || "";
|
|
91
91
|
this.maxRetries = config.maxRetries ?? 3;
|
|
92
92
|
this.debug = config.debug ?? false;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const missing = [
|
|
94
|
+
!this.baseUrl && "CONTROL_PLANE_URL",
|
|
95
|
+
!this.deploymentSecret && "DEPLOYMENT_SECRET",
|
|
96
|
+
!this.organizationId && "ORGANIZATION_ID",
|
|
97
|
+
!this.projectId && "PROJECT_ID",
|
|
98
|
+
!this.deploymentId && "DEPLOYMENT_ID"
|
|
99
|
+
].filter(Boolean);
|
|
100
|
+
if (missing.length > 0) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`[DataStoreClient] Missing required configuration: ${missing.join(", ")}. Pass them in the config object or set the corresponding environment variables.`
|
|
96
103
|
);
|
|
97
104
|
}
|
|
98
105
|
}
|
|
@@ -156,6 +163,8 @@ var DataStoreClient = class {
|
|
|
156
163
|
if (error instanceof AuthenticationError) throw error;
|
|
157
164
|
if (error instanceof DataStoreError && error.statusCode < 500) throw error;
|
|
158
165
|
if (attempt < this.maxRetries) {
|
|
166
|
+
const isIdempotent = method === "GET" || method === "HEAD";
|
|
167
|
+
if (!isIdempotent) throw lastError ?? error;
|
|
159
168
|
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
160
169
|
await new Promise((r) => setTimeout(r, delay));
|
|
161
170
|
}
|
|
@@ -163,7 +172,6 @@ var DataStoreClient = class {
|
|
|
163
172
|
}
|
|
164
173
|
throw lastError ?? new DataStoreError("Request failed", "UNKNOWN", 500);
|
|
165
174
|
}
|
|
166
|
-
// ─── Schema Operations ──────────────────────────────────────
|
|
167
175
|
async getSchema() {
|
|
168
176
|
return this.request("GET", "/schema");
|
|
169
177
|
}
|
|
@@ -183,7 +191,6 @@ var DataStoreClient = class {
|
|
|
183
191
|
async deleteColumn(tableName, columnName) {
|
|
184
192
|
return this.request("DELETE", "/schema/columns", { tableName, columnName });
|
|
185
193
|
}
|
|
186
|
-
// ─── Query Operations ───────────────────────────────────────
|
|
187
194
|
async query(tableName, options = {}) {
|
|
188
195
|
return this.request("POST", "/query", {
|
|
189
196
|
tableName,
|
|
@@ -217,7 +224,6 @@ var DataStoreClient = class {
|
|
|
217
224
|
primaryKey
|
|
218
225
|
});
|
|
219
226
|
}
|
|
220
|
-
// ─── Storage Operations ─────────────────────────────────────
|
|
221
227
|
async listFiles(options) {
|
|
222
228
|
const params = {};
|
|
223
229
|
if (options?.prefix) params.prefix = options.prefix;
|
package/dist/server/index.mjs
CHANGED
|
@@ -39,9 +39,16 @@ var DataStoreClient = class {
|
|
|
39
39
|
this.deploymentId = config.deploymentId || this.getEnv("DEPLOYMENT_ID") || "";
|
|
40
40
|
this.maxRetries = config.maxRetries ?? 3;
|
|
41
41
|
this.debug = config.debug ?? false;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const missing = [
|
|
43
|
+
!this.baseUrl && "CONTROL_PLANE_URL",
|
|
44
|
+
!this.deploymentSecret && "DEPLOYMENT_SECRET",
|
|
45
|
+
!this.organizationId && "ORGANIZATION_ID",
|
|
46
|
+
!this.projectId && "PROJECT_ID",
|
|
47
|
+
!this.deploymentId && "DEPLOYMENT_ID"
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
if (missing.length > 0) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`[DataStoreClient] Missing required configuration: ${missing.join(", ")}. Pass them in the config object or set the corresponding environment variables.`
|
|
45
52
|
);
|
|
46
53
|
}
|
|
47
54
|
}
|
|
@@ -105,6 +112,8 @@ var DataStoreClient = class {
|
|
|
105
112
|
if (error instanceof AuthenticationError) throw error;
|
|
106
113
|
if (error instanceof DataStoreError && error.statusCode < 500) throw error;
|
|
107
114
|
if (attempt < this.maxRetries) {
|
|
115
|
+
const isIdempotent = method === "GET" || method === "HEAD";
|
|
116
|
+
if (!isIdempotent) throw lastError ?? error;
|
|
108
117
|
const delay = Math.min(1e3 * 2 ** attempt, 1e4);
|
|
109
118
|
await new Promise((r) => setTimeout(r, delay));
|
|
110
119
|
}
|
|
@@ -112,7 +121,6 @@ var DataStoreClient = class {
|
|
|
112
121
|
}
|
|
113
122
|
throw lastError ?? new DataStoreError("Request failed", "UNKNOWN", 500);
|
|
114
123
|
}
|
|
115
|
-
// ─── Schema Operations ──────────────────────────────────────
|
|
116
124
|
async getSchema() {
|
|
117
125
|
return this.request("GET", "/schema");
|
|
118
126
|
}
|
|
@@ -132,7 +140,6 @@ var DataStoreClient = class {
|
|
|
132
140
|
async deleteColumn(tableName, columnName) {
|
|
133
141
|
return this.request("DELETE", "/schema/columns", { tableName, columnName });
|
|
134
142
|
}
|
|
135
|
-
// ─── Query Operations ───────────────────────────────────────
|
|
136
143
|
async query(tableName, options = {}) {
|
|
137
144
|
return this.request("POST", "/query", {
|
|
138
145
|
tableName,
|
|
@@ -166,7 +173,6 @@ var DataStoreClient = class {
|
|
|
166
173
|
primaryKey
|
|
167
174
|
});
|
|
168
175
|
}
|
|
169
|
-
// ─── Storage Operations ─────────────────────────────────────
|
|
170
176
|
async listFiles(options) {
|
|
171
177
|
const params = {};
|
|
172
178
|
if (options?.prefix) params.prefix = options.prefix;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stardeck-customer-apps/data-store-sdk",
|
|
3
|
-
"version": "0.1.0-preview.
|
|
3
|
+
"version": "0.1.0-preview.2",
|
|
4
4
|
"description": "SDK for accessing Stardeck data stores from deployed projects",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"stardeck-data-store": "./dist/cli/generate-types.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"SKILL.md"
|
|
17
18
|
],
|
|
18
19
|
"exports": {
|
|
19
20
|
".": {
|