smoodly 0.0.8 → 0.0.9

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.
@@ -8,6 +8,7 @@
8
8
  // untouched.
9
9
  import { collectionDepth, collectionOrder } from "./collections.js";
10
10
  import { DEFAULT_ENTRY_LOCALES, entryErrors, entryFieldsIn, entryLocaleRow, entryRefEdges, entryTitleIn, isSharedSchema, schemaOf, splitFields, } from "./entry-store.js";
11
+ import { sharedEntryId } from "./shared-id.js";
11
12
  import { SupabasePathIndex } from "./supabase-path-index.js";
12
13
  import { assertDepth, assertSupportedLocale, entryPathRows, slugOf } from "./paths.js";
13
14
  // entries.id is a Postgres uuid column: filtering on a non-UUID string
@@ -54,6 +55,29 @@ export class SupabaseEntryStore {
54
55
  schema(collection) {
55
56
  return schemaOf(this.schemas, collection);
56
57
  }
58
+ /** The space this client acts in — the ONE function the policies and
59
+ * the insert trigger read (src/sql-space.ts): a key's claim in the
60
+ * cloud, the default space under the service role. Constant for the
61
+ * life of the client, so the PROMISE is cached and concurrent creates
62
+ * share one round trip; a failed lookup is not cached. */
63
+ space() {
64
+ if (!this.spaceId) {
65
+ this.spaceId = this.fetchSpace().catch((e) => {
66
+ // A failed lookup is not cached: the next write retries it.
67
+ this.spaceId = undefined;
68
+ throw e;
69
+ });
70
+ }
71
+ return this.spaceId;
72
+ }
73
+ async fetchSpace() {
74
+ const res = await this.db.rpc("smoodly_current_space");
75
+ if (res.error)
76
+ this.fail(res.error.message);
77
+ if (typeof res.data !== "string")
78
+ this.fail("smoodly_current_space() returned no space — is the schema up to date?");
79
+ return res.data;
80
+ }
57
81
  // ── reads ──
58
82
  async fetch(collection, id) {
59
83
  if (!UUID_RE.test(id))
@@ -253,9 +277,20 @@ export class SupabaseEntryStore {
253
277
  this.assertParentHasLocale(parent, options.locale);
254
278
  const { shared, localized, slug } = splitFields(fields, schema);
255
279
  await this.assertSiblingSlug(collection, parentId, options.locale, slug);
256
- const inserted = await this.db.from("entries").insert({ collection, parent_id: parentId, fields: shared }).select("id").single();
257
- if (inserted.error)
280
+ // A shared item's row id is DERIVED from (space, name), not generated
281
+ // (spec 2026-09-07 §10): the SELECT above is a fast path two racing
282
+ // materializations can both pass, so the primary key is the backstop
283
+ // — a duplicate would be unremovable, since `delete` refuses every
284
+ // shared row.
285
+ const row = { collection, parent_id: parentId, fields: shared };
286
+ if (isSharedSchema(schema))
287
+ row.id = await sharedEntryId(await this.space(), collection);
288
+ const inserted = await this.db.from("entries").insert(row).select("id").single();
289
+ if (inserted.error) {
290
+ if (isSharedSchema(schema) && inserted.error.code === UNIQUE_VIOLATION)
291
+ throw entryErrors.singleton(collection);
258
292
  this.fail(inserted.error.message);
293
+ }
259
294
  const id = inserted.data.id;
260
295
  try {
261
296
  await this.insertLocale(id, options.locale, { slug, fields: localized });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoodly",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Open-source, code-first CMS for Next.js on Supabase: define a component once, it becomes a typed, editable building block.",
5
5
  "license": "MIT",
6
6
  "repository": {