@stardeck-customer-apps/data-store-sdk 0.3.2 → 0.4.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.
@@ -88,18 +88,29 @@ async function introspectSchemaSnapshot(executor) {
88
88
  ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
89
89
  ),
90
90
  executor.query(
91
- `SELECT tc.constraint_name, tc.table_name, ku.column_name,
92
- ccu.table_name AS referenced_table, ccu.column_name AS referenced_column,
93
- rc.delete_rule, rc.update_rule
94
- FROM information_schema.table_constraints tc
95
- JOIN information_schema.key_column_usage ku
96
- ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
97
- JOIN information_schema.constraint_column_usage ccu
98
- ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
99
- JOIN information_schema.referential_constraints rc
100
- ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema
101
- WHERE tc.table_schema = 'public' AND tc.constraint_type = 'FOREIGN KEY'
102
- ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
91
+ // Read column pairings from pg_catalog: information_schema's
92
+ // constraint_column_usage is uncorrelated with key_column_usage, so
93
+ // joining them for a composite FK produces a cross product. Unnesting
94
+ // conkey/confkey together WITH ORDINALITY keeps referencing↔referenced
95
+ // columns paired and ordered.
96
+ `SELECT con.conname AS constraint_name,
97
+ rel.relname AS table_name,
98
+ att.attname AS column_name,
99
+ frel.relname AS referenced_table,
100
+ fatt.attname AS referenced_column,
101
+ CASE con.confdeltype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT'
102
+ WHEN 'c' THEN 'CASCADE' WHEN 'n' THEN 'SET NULL' WHEN 'd' THEN 'SET DEFAULT' END AS delete_rule,
103
+ CASE con.confupdtype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT'
104
+ WHEN 'c' THEN 'CASCADE' WHEN 'n' THEN 'SET NULL' WHEN 'd' THEN 'SET DEFAULT' END AS update_rule
105
+ FROM pg_constraint con
106
+ JOIN pg_class rel ON rel.oid = con.conrelid
107
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
108
+ JOIN pg_class frel ON frel.oid = con.confrelid
109
+ JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS cols(conkey, confkey, ord) ON true
110
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = cols.conkey
111
+ JOIN pg_attribute fatt ON fatt.attrelid = con.confrelid AND fatt.attnum = cols.confkey
112
+ WHERE ns.nspname = 'public' AND con.contype = 'f'
113
+ ORDER BY rel.relname, con.conname, cols.ord`
103
114
  ),
104
115
  executor.query(
105
116
  `SELECT rel.relname AS table_name, con.conname AS name,
@@ -65,18 +65,29 @@ async function introspectSchemaSnapshot(executor) {
65
65
  ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
66
66
  ),
67
67
  executor.query(
68
- `SELECT tc.constraint_name, tc.table_name, ku.column_name,
69
- ccu.table_name AS referenced_table, ccu.column_name AS referenced_column,
70
- rc.delete_rule, rc.update_rule
71
- FROM information_schema.table_constraints tc
72
- JOIN information_schema.key_column_usage ku
73
- ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
74
- JOIN information_schema.constraint_column_usage ccu
75
- ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
76
- JOIN information_schema.referential_constraints rc
77
- ON tc.constraint_name = rc.constraint_name AND tc.table_schema = rc.constraint_schema
78
- WHERE tc.table_schema = 'public' AND tc.constraint_type = 'FOREIGN KEY'
79
- ORDER BY tc.table_name, tc.constraint_name, ku.ordinal_position`
68
+ // Read column pairings from pg_catalog: information_schema's
69
+ // constraint_column_usage is uncorrelated with key_column_usage, so
70
+ // joining them for a composite FK produces a cross product. Unnesting
71
+ // conkey/confkey together WITH ORDINALITY keeps referencing↔referenced
72
+ // columns paired and ordered.
73
+ `SELECT con.conname AS constraint_name,
74
+ rel.relname AS table_name,
75
+ att.attname AS column_name,
76
+ frel.relname AS referenced_table,
77
+ fatt.attname AS referenced_column,
78
+ CASE con.confdeltype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT'
79
+ WHEN 'c' THEN 'CASCADE' WHEN 'n' THEN 'SET NULL' WHEN 'd' THEN 'SET DEFAULT' END AS delete_rule,
80
+ CASE con.confupdtype WHEN 'a' THEN 'NO ACTION' WHEN 'r' THEN 'RESTRICT'
81
+ WHEN 'c' THEN 'CASCADE' WHEN 'n' THEN 'SET NULL' WHEN 'd' THEN 'SET DEFAULT' END AS update_rule
82
+ FROM pg_constraint con
83
+ JOIN pg_class rel ON rel.oid = con.conrelid
84
+ JOIN pg_namespace ns ON ns.oid = rel.relnamespace
85
+ JOIN pg_class frel ON frel.oid = con.confrelid
86
+ JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS cols(conkey, confkey, ord) ON true
87
+ JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = cols.conkey
88
+ JOIN pg_attribute fatt ON fatt.attrelid = con.confrelid AND fatt.attnum = cols.confkey
89
+ WHERE ns.nspname = 'public' AND con.contype = 'f'
90
+ ORDER BY rel.relname, con.conname, cols.ord`
80
91
  ),
81
92
  executor.query(
82
93
  `SELECT rel.relname AS table_name, con.conname AS name,
@@ -140,7 +140,19 @@ interface DataStoreManifestEntry {
140
140
  id: string;
141
141
  name: string;
142
142
  slug: string;
143
- url: string;
143
+ /**
144
+ * App-local handle for this project's connection. When present, the resolver
145
+ * matches it before the org-global slug/name.
146
+ */
147
+ bindingKey?: string;
148
+ /**
149
+ * Store kind. `database` stores carry a `url`; `storage` stores do not (access
150
+ * is API-mediated by store id). Absent on manifests emitted before this field
151
+ * existed — treat absent as `database`.
152
+ */
153
+ storeType?: "database" | "storage";
154
+ /** Connection string. Present for `database` stores only. */
155
+ url?: string;
144
156
  accessLevel: "read" | "write" | "admin";
145
157
  }
146
158
  /**
@@ -140,7 +140,19 @@ interface DataStoreManifestEntry {
140
140
  id: string;
141
141
  name: string;
142
142
  slug: string;
143
- url: string;
143
+ /**
144
+ * App-local handle for this project's connection. When present, the resolver
145
+ * matches it before the org-global slug/name.
146
+ */
147
+ bindingKey?: string;
148
+ /**
149
+ * Store kind. `database` stores carry a `url`; `storage` stores do not (access
150
+ * is API-mediated by store id). Absent on manifests emitted before this field
151
+ * existed — treat absent as `database`.
152
+ */
153
+ storeType?: "database" | "storage";
154
+ /** Connection string. Present for `database` stores only. */
155
+ url?: string;
144
156
  accessLevel: "read" | "write" | "admin";
145
157
  }
146
158
  /**
@@ -94,9 +94,14 @@ function readDataStoreManifest() {
94
94
  try {
95
95
  const parsed = JSON.parse(raw);
96
96
  if (!Array.isArray(parsed)) return [];
97
- return parsed.filter(
98
- (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.name === "string" && typeof e.slug === "string" && typeof e.url === "string"
99
- );
97
+ return parsed.filter((e) => {
98
+ if (typeof e !== "object" || e === null) return false;
99
+ const entry = e;
100
+ if (typeof entry.id !== "string" || typeof entry.name !== "string" || typeof entry.slug !== "string") {
101
+ return false;
102
+ }
103
+ return entry.storeType === "storage" || typeof entry.url === "string";
104
+ });
100
105
  } catch {
101
106
  return [];
102
107
  }
@@ -111,9 +116,13 @@ function resolveManifestEntry(entries, ref) {
111
116
  }
112
117
  if (ref.storeName) {
113
118
  const target = ref.storeName;
119
+ const byBindingKey = entries.find((e) => e.bindingKey === target);
120
+ if (byBindingKey) return byBindingKey;
114
121
  const exact = entries.find((e) => e.slug === target || e.id === target || e.name === target);
115
122
  if (exact) return exact;
116
123
  const norm = dataStoreSlug(target);
124
+ const byBindingKeyNorm = entries.find((e) => e.bindingKey === norm);
125
+ if (byBindingKeyNorm) return byBindingKeyNorm;
117
126
  return entries.find(
118
127
  (e) => e.slug === norm || typeof e.name === "string" && dataStoreSlug(e.name) === norm
119
128
  );
@@ -324,6 +333,12 @@ async function createDataStore(options) {
324
333
  if (!connectionString) {
325
334
  const name = options?.storeName ?? options?.storeId;
326
335
  const ref = name ? `data store "${name}"` : "a data store";
336
+ const resolved = options?.storeId || options?.storeName ? resolveDataStore({ storeId: options?.storeId, storeName: options?.storeName }) : void 0;
337
+ if (resolved?.storeType === "storage") {
338
+ throw new Error(
339
+ `${ref} is a storage store and has no SQL connection. Use DataStoreClient for storage operations (upload/download/list) instead of createDataStore.`
340
+ );
341
+ }
327
342
  throw new Error(
328
343
  `Could not resolve a connection string for ${ref}. Checked the STARDECK_DATA_STORES manifest, the legacy DATA_STORE_*_URL env var, and DATA_STORE_URL. Confirm the store is connected to this project, or pass connectionString in options.`
329
344
  );
@@ -41,9 +41,14 @@ function readDataStoreManifest() {
41
41
  try {
42
42
  const parsed = JSON.parse(raw);
43
43
  if (!Array.isArray(parsed)) return [];
44
- return parsed.filter(
45
- (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.name === "string" && typeof e.slug === "string" && typeof e.url === "string"
46
- );
44
+ return parsed.filter((e) => {
45
+ if (typeof e !== "object" || e === null) return false;
46
+ const entry = e;
47
+ if (typeof entry.id !== "string" || typeof entry.name !== "string" || typeof entry.slug !== "string") {
48
+ return false;
49
+ }
50
+ return entry.storeType === "storage" || typeof entry.url === "string";
51
+ });
47
52
  } catch {
48
53
  return [];
49
54
  }
@@ -58,9 +63,13 @@ function resolveManifestEntry(entries, ref) {
58
63
  }
59
64
  if (ref.storeName) {
60
65
  const target = ref.storeName;
66
+ const byBindingKey = entries.find((e) => e.bindingKey === target);
67
+ if (byBindingKey) return byBindingKey;
61
68
  const exact = entries.find((e) => e.slug === target || e.id === target || e.name === target);
62
69
  if (exact) return exact;
63
70
  const norm = dataStoreSlug(target);
71
+ const byBindingKeyNorm = entries.find((e) => e.bindingKey === norm);
72
+ if (byBindingKeyNorm) return byBindingKeyNorm;
64
73
  return entries.find(
65
74
  (e) => e.slug === norm || typeof e.name === "string" && dataStoreSlug(e.name) === norm
66
75
  );
@@ -271,6 +280,12 @@ async function createDataStore(options) {
271
280
  if (!connectionString) {
272
281
  const name = options?.storeName ?? options?.storeId;
273
282
  const ref = name ? `data store "${name}"` : "a data store";
283
+ const resolved = options?.storeId || options?.storeName ? resolveDataStore({ storeId: options?.storeId, storeName: options?.storeName }) : void 0;
284
+ if (resolved?.storeType === "storage") {
285
+ throw new Error(
286
+ `${ref} is a storage store and has no SQL connection. Use DataStoreClient for storage operations (upload/download/list) instead of createDataStore.`
287
+ );
288
+ }
274
289
  throw new Error(
275
290
  `Could not resolve a connection string for ${ref}. Checked the STARDECK_DATA_STORES manifest, the legacy DATA_STORE_*_URL env var, and DATA_STORE_URL. Confirm the store is connected to this project, or pass connectionString in options.`
276
291
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stardeck-customer-apps/data-store-sdk",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "SDK for accessing Stardeck data stores from deployed projects",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",