@ai-matrx/associations 0.9.33 → 0.10.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.
@@ -372,9 +372,19 @@ type EntityRowResult = {
372
372
  interface CreateEntityRowArgs {
373
373
  /** The new row's human name — written to the registry `titleColumn`. */
374
374
  title: string;
375
- /** Org to stamp (skipped when the token has no org column). */
375
+ /**
376
+ * The organization the new row belongs to. REQUIRED by
377
+ * `public.entity_row_create`: every creatable token's table carries
378
+ * `organization_id`, and the door refuses a row with nobody owning it.
379
+ */
376
380
  orgId?: string | null;
377
- /** Required NOT NULL columns the registry conventions can't know about. */
381
+ /**
382
+ * 🚨 NO LONGER WRITABLE THROUGH THIS SERVICE (DOORS-ONLY-4). The door writes the
383
+ * name, the owner and the organization and nothing else, so a non-empty value here
384
+ * is REFUSED with the columns named rather than dropped on the floor. A token whose
385
+ * table needs more than a name is created by its own feature's create service; this
386
+ * generic path then only renames it.
387
+ */
378
388
  extraColumns?: Record<string, unknown>;
379
389
  }
380
390
  interface EntityRowsServiceApi {
@@ -372,9 +372,19 @@ type EntityRowResult = {
372
372
  interface CreateEntityRowArgs {
373
373
  /** The new row's human name — written to the registry `titleColumn`. */
374
374
  title: string;
375
- /** Org to stamp (skipped when the token has no org column). */
375
+ /**
376
+ * The organization the new row belongs to. REQUIRED by
377
+ * `public.entity_row_create`: every creatable token's table carries
378
+ * `organization_id`, and the door refuses a row with nobody owning it.
379
+ */
376
380
  orgId?: string | null;
377
- /** Required NOT NULL columns the registry conventions can't know about. */
381
+ /**
382
+ * 🚨 NO LONGER WRITABLE THROUGH THIS SERVICE (DOORS-ONLY-4). The door writes the
383
+ * name, the owner and the organization and nothing else, so a non-empty value here
384
+ * is REFUSED with the columns named rather than dropped on the floor. A token whose
385
+ * table needs more than a name is created by its own feature's create service; this
386
+ * generic path then only renames it.
387
+ */
378
388
  extraColumns?: Record<string, unknown>;
379
389
  }
380
390
  interface EntityRowsServiceApi {
@@ -3143,31 +3143,7 @@ function createTitlesService(deps, candidates) {
3143
3143
 
3144
3144
  // src/core/entityRows.ts
3145
3145
  function createEntityRowsService(deps, titles) {
3146
- const { dataSource, identity, registry, errorSink } = deps;
3147
- function tableFor(token, schema, table) {
3148
- if (schema && schema !== "public") {
3149
- if (!dataSource.schema) {
3150
- const error = `dataSource has no schema() capability \u2014 entity-row writes for "${token}" (${schema}.${table}) need a table-capable dataSource (a real supabase-js client)`;
3151
- errorSink({
3152
- code: "datasource_missing_table_capability",
3153
- message: `[entityRows] ${error}`,
3154
- context: { token, schema, table }
3155
- });
3156
- return { ok: false, error };
3157
- }
3158
- return { ok: true, query: dataSource.schema(schema).from(table) };
3159
- }
3160
- if (!dataSource.from) {
3161
- const error = `dataSource has no from() capability \u2014 entity-row writes for "${token}" need a table-capable dataSource (a real supabase-js client)`;
3162
- errorSink({
3163
- code: "datasource_missing_table_capability",
3164
- message: `[entityRows] ${error}`,
3165
- context: { token, schema, table }
3166
- });
3167
- return { ok: false, error };
3168
- }
3169
- return { ok: true, query: dataSource.from(table) };
3170
- }
3146
+ const { dataSource, registry, errorSink } = deps;
3171
3147
  function requireTitleColumn(token, op) {
3172
3148
  const info = registry.getEntityInfo(token);
3173
3149
  if (!info.titleColumn) {
@@ -3179,43 +3155,68 @@ function createEntityRowsService(deps, titles) {
3179
3155
  });
3180
3156
  return { ok: false, error };
3181
3157
  }
3182
- return { ok: true, titleColumn: info.titleColumn };
3158
+ return { ok: true };
3183
3159
  }
3184
3160
  return {
3185
3161
  /**
3186
- * Insert one row of `token`'s backing table named `title`, owned by the
3187
- * current user (when the table has an owner column). Returns the new
3188
- * row's id + title. The caller wires associations afterwards — this
3189
- * layer never writes edges.
3162
+ * Create one row of `token`'s backing table named `title`, through
3163
+ * `public.entity_row_create`. The door stamps `created_by` from `auth.uid()`,
3164
+ * checks the caller can act in the organization, and refuses machinery tokens and
3165
+ * tables with no organization by name. Returns the new row's id + title. The
3166
+ * caller wires associations afterwards — this layer never writes edges.
3190
3167
  */
3191
3168
  async createEntityRow(token, args) {
3192
3169
  const gate = requireTitleColumn(token, "creating rows");
3193
3170
  if (!gate.ok) return gate;
3194
- const info = registry.getEntityInfo(token);
3195
3171
  const title = args.title.trim();
3196
3172
  if (!title) return { ok: false, error: "Name is required" };
3173
+ const extras = Object.keys(args.extraColumns ?? {});
3174
+ if (extras.length > 0) {
3175
+ const error = `A new ${registry.getEntityInfo(token).label} cannot be created here with ${extras.join(", ")}: the shared create door writes the name, the owner and the organization and nothing else. Create it with this feature's own create service, then attach it.`;
3176
+ errorSink({
3177
+ code: "entity_row_extra_columns_unsupported",
3178
+ message: `[createEntityRow] extraColumns are not writable through entity_row_create for "${token}": ${extras.join(", ")}`,
3179
+ context: { token, extras }
3180
+ });
3181
+ return { ok: false, error };
3182
+ }
3183
+ if (!args.orgId) {
3184
+ const error = `Name the organization this ${registry.getEntityInfo(token).label} belongs to.`;
3185
+ errorSink({
3186
+ code: "entity_row_missing_organization",
3187
+ message: `[createEntityRow] no organization supplied for "${token}"; entity_row_create requires one`,
3188
+ context: { token }
3189
+ });
3190
+ return { ok: false, error };
3191
+ }
3197
3192
  try {
3198
- const userId = identity.requireUserId();
3199
- const table = tableFor(token, info.schema, info.table);
3200
- if (!table.ok) return table;
3201
- const row = {
3202
- [gate.titleColumn]: title,
3203
- ...info.ownerColumn ? { [info.ownerColumn]: userId } : {},
3204
- ...info.orgColumn && args.orgId ? { [info.orgColumn]: args.orgId } : {},
3205
- ...args.extraColumns ?? {}
3206
- };
3207
- const { data, error } = await table.query.insert(row).select("id").single();
3193
+ const { data, error } = await dataSource.rpc("entity_row_create", {
3194
+ p_token: token,
3195
+ p_title: title,
3196
+ p_organization_id: args.orgId
3197
+ });
3208
3198
  if (error) {
3209
3199
  errorSink({
3210
3200
  code: "entity_row_insert_failed",
3211
- message: `[createEntityRow] insert failed for "${token}": ${error.message}`,
3201
+ message: `[createEntityRow] entity_row_create failed for "${token}": ${error.message}`,
3212
3202
  context: { token, detail: error }
3213
3203
  });
3214
3204
  return { ok: false, error: error.message };
3215
3205
  }
3216
- const id = String(data.id);
3217
- titles.prime(token, id, title);
3218
- return { ok: true, data: { id, title } };
3206
+ const row = data;
3207
+ if (!row || typeof row.id !== "string" || !row.id) {
3208
+ const msg = "The create door returned no record.";
3209
+ errorSink({
3210
+ code: "entity_row_insert_failed",
3211
+ message: `[createEntityRow] entity_row_create returned no id for "${token}"`,
3212
+ context: { token, detail: data }
3213
+ });
3214
+ return { ok: false, error: msg };
3215
+ }
3216
+ const id = row.id;
3217
+ const named = typeof row.title === "string" && row.title ? row.title : title;
3218
+ titles.prime(token, id, named);
3219
+ return { ok: true, data: { id, title: named } };
3219
3220
  } catch (e) {
3220
3221
  const msg = e instanceof Error ? e.message : "Failed to create";
3221
3222
  errorSink({
@@ -3226,21 +3227,27 @@ function createEntityRowsService(deps, titles) {
3226
3227
  return { ok: false, error: msg };
3227
3228
  }
3228
3229
  },
3229
- /** Update the registry `titleColumn` of one row of `token`'s table. */
3230
+ /**
3231
+ * Rename one row of `token`'s table through `public.entity_row_rename`. The door
3232
+ * decides on the one ladder — the row's author, `editor` on the row, or a platform
3233
+ * admin — and answers "there is no such thing here" rather than telling a caller
3234
+ * that somebody else's row exists.
3235
+ */
3230
3236
  async renameEntityRow(token, id, title) {
3231
3237
  const gate = requireTitleColumn(token, "renaming rows");
3232
3238
  if (!gate.ok) return gate;
3233
- const info = registry.getEntityInfo(token);
3234
3239
  const trimmed = title.trim();
3235
3240
  if (!trimmed) return { ok: false, error: "Name is required" };
3236
3241
  try {
3237
- const table = tableFor(token, info.schema, info.table);
3238
- if (!table.ok) return table;
3239
- const { error } = await table.query.update({ [gate.titleColumn]: trimmed }).eq("id", id);
3242
+ const { error } = await dataSource.rpc("entity_row_rename", {
3243
+ p_token: token,
3244
+ p_id: id,
3245
+ p_title: trimmed
3246
+ });
3240
3247
  if (error) {
3241
3248
  errorSink({
3242
3249
  code: "entity_row_update_failed",
3243
- message: `[renameEntityRow] update failed for "${token}": ${error.message}`,
3250
+ message: `[renameEntityRow] entity_row_rename failed for "${token}": ${error.message}`,
3244
3251
  context: { token, id, detail: error }
3245
3252
  });
3246
3253
  return { ok: false, error: error.message };