@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.
@@ -276,6 +276,22 @@ interface DemandedRpcSignatures {
276
276
  title: string;
277
277
  }[];
278
278
  };
279
+ entity_row_create: {
280
+ Args: {
281
+ p_organization_id: string;
282
+ p_title: string;
283
+ p_token: string;
284
+ };
285
+ Returns: unknown;
286
+ };
287
+ entity_row_rename: {
288
+ Args: {
289
+ p_id: string;
290
+ p_title: string;
291
+ p_token: string;
292
+ };
293
+ Returns: unknown;
294
+ };
279
295
  cmt_list: {
280
296
  Args: {
281
297
  p_entity_id: string;
@@ -364,27 +380,6 @@ interface PgError {
364
380
  hint?: string | null;
365
381
  }
366
382
 
367
- /**
368
- * The minimal table surface entity-row create/rename needs (a structural
369
- * subset of supabase-js's query builder). Registry-convention writes go
370
- * DIRECT to the backing table (they are RLS-gated feature rows, not
371
- * association edges — there is no RPC for them by design).
372
- */
373
- interface AssociationsTableQuery {
374
- insert(row: Record<string, unknown>): {
375
- select(columns: string): {
376
- single(): PromiseLike<{
377
- data: unknown;
378
- error: PgError | null;
379
- }>;
380
- };
381
- };
382
- update(patch: Record<string, unknown>): {
383
- eq(column: string, value: string): PromiseLike<{
384
- error: PgError | null;
385
- }>;
386
- };
387
- }
388
383
  /**
389
384
  * The data seam — a structural subset of `SupabaseClient<Database>`: any
390
385
  * client that can answer the demanded RPC families (README § The demanded
@@ -392,22 +387,21 @@ interface AssociationsTableQuery {
392
387
  * one. `PromiseLike` because supabase-js returns a thenable builder, not a
393
388
  * `Promise`.
394
389
  *
395
- * `from`/`schema` are OPTIONAL (W2, additive): only the entity-row
396
- * create/rename service uses them (generic registry-convention row writes).
397
- * A dataSource without them still satisfies every RPC operation; calling
398
- * entity-row create/rename against it returns a screamed `{ok:false}`
399
- * result naming the missing capability — never a silent no-op. A real
400
- * supabase-js client satisfies both structurally.
390
+ * 🚨 RPC AND NOTHING ELSE (DOORS-ONLY-4, 2026-09-21). This port used to carry
391
+ * OPTIONAL `from`/`schema` table surfaces, used by exactly one service —
392
+ * entity-row create/rename, which wrote the registered entity's backing table
393
+ * directly. The doors-only ruling (`platform` and `iam` are not client-writable;
394
+ * every write goes through a named SECURITY DEFINER door) made that path answer
395
+ * `42501` on every closed table, so it now calls `entity_row_create` /
396
+ * `entity_row_rename` like every other operation. With its one reader gone the
397
+ * two optional members were a port nobody reads — removed rather than left as a
398
+ * seam a future write could quietly reach for.
401
399
  */
402
400
  interface AssociationsDataSource {
403
401
  rpc(fn: DemandedRpcName, args: Record<string, unknown>): PromiseLike<{
404
402
  data: unknown;
405
403
  error: PgError | null;
406
404
  }>;
407
- from?(table: string): AssociationsTableQuery;
408
- schema?(name: string): {
409
- from(table: string): AssociationsTableQuery;
410
- };
411
405
  }
412
406
  /**
413
407
  * The identity seam — today a store-singleton read (`requireUserId`) plus the
@@ -1100,9 +1094,19 @@ type EntityRowResult = {
1100
1094
  interface CreateEntityRowArgs {
1101
1095
  /** The new row's human name — written to the registry `titleColumn`. */
1102
1096
  title: string;
1103
- /** Org to stamp (skipped when the token has no org column). */
1097
+ /**
1098
+ * The organization the new row belongs to. REQUIRED by
1099
+ * `public.entity_row_create`: every creatable token's table carries
1100
+ * `organization_id`, and the door refuses a row with nobody owning it.
1101
+ */
1104
1102
  orgId?: string | null;
1105
- /** Required NOT NULL columns the registry conventions can't know about. */
1103
+ /**
1104
+ * 🚨 NO LONGER WRITABLE THROUGH THIS SERVICE (DOORS-ONLY-4). The door writes the
1105
+ * name, the owner and the organization and nothing else, so a non-empty value here
1106
+ * is REFUSED with the columns named rather than dropped on the floor. A token whose
1107
+ * table needs more than a name is created by its own feature's create service; this
1108
+ * generic path then only renames it.
1109
+ */
1106
1110
  extraColumns?: Record<string, unknown>;
1107
1111
  }
1108
1112
  interface EntityRowsServiceApi {
@@ -276,6 +276,22 @@ interface DemandedRpcSignatures {
276
276
  title: string;
277
277
  }[];
278
278
  };
279
+ entity_row_create: {
280
+ Args: {
281
+ p_organization_id: string;
282
+ p_title: string;
283
+ p_token: string;
284
+ };
285
+ Returns: unknown;
286
+ };
287
+ entity_row_rename: {
288
+ Args: {
289
+ p_id: string;
290
+ p_title: string;
291
+ p_token: string;
292
+ };
293
+ Returns: unknown;
294
+ };
279
295
  cmt_list: {
280
296
  Args: {
281
297
  p_entity_id: string;
@@ -364,27 +380,6 @@ interface PgError {
364
380
  hint?: string | null;
365
381
  }
366
382
 
367
- /**
368
- * The minimal table surface entity-row create/rename needs (a structural
369
- * subset of supabase-js's query builder). Registry-convention writes go
370
- * DIRECT to the backing table (they are RLS-gated feature rows, not
371
- * association edges — there is no RPC for them by design).
372
- */
373
- interface AssociationsTableQuery {
374
- insert(row: Record<string, unknown>): {
375
- select(columns: string): {
376
- single(): PromiseLike<{
377
- data: unknown;
378
- error: PgError | null;
379
- }>;
380
- };
381
- };
382
- update(patch: Record<string, unknown>): {
383
- eq(column: string, value: string): PromiseLike<{
384
- error: PgError | null;
385
- }>;
386
- };
387
- }
388
383
  /**
389
384
  * The data seam — a structural subset of `SupabaseClient<Database>`: any
390
385
  * client that can answer the demanded RPC families (README § The demanded
@@ -392,22 +387,21 @@ interface AssociationsTableQuery {
392
387
  * one. `PromiseLike` because supabase-js returns a thenable builder, not a
393
388
  * `Promise`.
394
389
  *
395
- * `from`/`schema` are OPTIONAL (W2, additive): only the entity-row
396
- * create/rename service uses them (generic registry-convention row writes).
397
- * A dataSource without them still satisfies every RPC operation; calling
398
- * entity-row create/rename against it returns a screamed `{ok:false}`
399
- * result naming the missing capability — never a silent no-op. A real
400
- * supabase-js client satisfies both structurally.
390
+ * 🚨 RPC AND NOTHING ELSE (DOORS-ONLY-4, 2026-09-21). This port used to carry
391
+ * OPTIONAL `from`/`schema` table surfaces, used by exactly one service —
392
+ * entity-row create/rename, which wrote the registered entity's backing table
393
+ * directly. The doors-only ruling (`platform` and `iam` are not client-writable;
394
+ * every write goes through a named SECURITY DEFINER door) made that path answer
395
+ * `42501` on every closed table, so it now calls `entity_row_create` /
396
+ * `entity_row_rename` like every other operation. With its one reader gone the
397
+ * two optional members were a port nobody reads — removed rather than left as a
398
+ * seam a future write could quietly reach for.
401
399
  */
402
400
  interface AssociationsDataSource {
403
401
  rpc(fn: DemandedRpcName, args: Record<string, unknown>): PromiseLike<{
404
402
  data: unknown;
405
403
  error: PgError | null;
406
404
  }>;
407
- from?(table: string): AssociationsTableQuery;
408
- schema?(name: string): {
409
- from(table: string): AssociationsTableQuery;
410
- };
411
405
  }
412
406
  /**
413
407
  * The identity seam — today a store-singleton read (`requireUserId`) plus the
@@ -1100,9 +1094,19 @@ type EntityRowResult = {
1100
1094
  interface CreateEntityRowArgs {
1101
1095
  /** The new row's human name — written to the registry `titleColumn`. */
1102
1096
  title: string;
1103
- /** Org to stamp (skipped when the token has no org column). */
1097
+ /**
1098
+ * The organization the new row belongs to. REQUIRED by
1099
+ * `public.entity_row_create`: every creatable token's table carries
1100
+ * `organization_id`, and the door refuses a row with nobody owning it.
1101
+ */
1104
1102
  orgId?: string | null;
1105
- /** Required NOT NULL columns the registry conventions can't know about. */
1103
+ /**
1104
+ * 🚨 NO LONGER WRITABLE THROUGH THIS SERVICE (DOORS-ONLY-4). The door writes the
1105
+ * name, the owner and the organization and nothing else, so a non-empty value here
1106
+ * is REFUSED with the columns named rather than dropped on the floor. A token whose
1107
+ * table needs more than a name is created by its own feature's create service; this
1108
+ * generic path then only renames it.
1109
+ */
1106
1110
  extraColumns?: Record<string, unknown>;
1107
1111
  }
1108
1112
  interface EntityRowsServiceApi {
@@ -3195,31 +3195,7 @@ function createTitlesService(deps, candidates) {
3195
3195
 
3196
3196
  // src/core/entityRows.ts
3197
3197
  function createEntityRowsService(deps, titles) {
3198
- const { dataSource, identity, registry, errorSink } = deps;
3199
- function tableFor(token, schema, table) {
3200
- if (schema && schema !== "public") {
3201
- if (!dataSource.schema) {
3202
- 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)`;
3203
- errorSink({
3204
- code: "datasource_missing_table_capability",
3205
- message: `[entityRows] ${error}`,
3206
- context: { token, schema, table }
3207
- });
3208
- return { ok: false, error };
3209
- }
3210
- return { ok: true, query: dataSource.schema(schema).from(table) };
3211
- }
3212
- if (!dataSource.from) {
3213
- const error = `dataSource has no from() capability \u2014 entity-row writes for "${token}" need a table-capable dataSource (a real supabase-js client)`;
3214
- errorSink({
3215
- code: "datasource_missing_table_capability",
3216
- message: `[entityRows] ${error}`,
3217
- context: { token, schema, table }
3218
- });
3219
- return { ok: false, error };
3220
- }
3221
- return { ok: true, query: dataSource.from(table) };
3222
- }
3198
+ const { dataSource, registry, errorSink } = deps;
3223
3199
  function requireTitleColumn(token, op) {
3224
3200
  const info = registry.getEntityInfo(token);
3225
3201
  if (!info.titleColumn) {
@@ -3231,43 +3207,68 @@ function createEntityRowsService(deps, titles) {
3231
3207
  });
3232
3208
  return { ok: false, error };
3233
3209
  }
3234
- return { ok: true, titleColumn: info.titleColumn };
3210
+ return { ok: true };
3235
3211
  }
3236
3212
  return {
3237
3213
  /**
3238
- * Insert one row of `token`'s backing table named `title`, owned by the
3239
- * current user (when the table has an owner column). Returns the new
3240
- * row's id + title. The caller wires associations afterwards — this
3241
- * layer never writes edges.
3214
+ * Create one row of `token`'s backing table named `title`, through
3215
+ * `public.entity_row_create`. The door stamps `created_by` from `auth.uid()`,
3216
+ * checks the caller can act in the organization, and refuses machinery tokens and
3217
+ * tables with no organization by name. Returns the new row's id + title. The
3218
+ * caller wires associations afterwards — this layer never writes edges.
3242
3219
  */
3243
3220
  async createEntityRow(token, args) {
3244
3221
  const gate = requireTitleColumn(token, "creating rows");
3245
3222
  if (!gate.ok) return gate;
3246
- const info = registry.getEntityInfo(token);
3247
3223
  const title = args.title.trim();
3248
3224
  if (!title) return { ok: false, error: "Name is required" };
3225
+ const extras = Object.keys(args.extraColumns ?? {});
3226
+ if (extras.length > 0) {
3227
+ 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.`;
3228
+ errorSink({
3229
+ code: "entity_row_extra_columns_unsupported",
3230
+ message: `[createEntityRow] extraColumns are not writable through entity_row_create for "${token}": ${extras.join(", ")}`,
3231
+ context: { token, extras }
3232
+ });
3233
+ return { ok: false, error };
3234
+ }
3235
+ if (!args.orgId) {
3236
+ const error = `Name the organization this ${registry.getEntityInfo(token).label} belongs to.`;
3237
+ errorSink({
3238
+ code: "entity_row_missing_organization",
3239
+ message: `[createEntityRow] no organization supplied for "${token}"; entity_row_create requires one`,
3240
+ context: { token }
3241
+ });
3242
+ return { ok: false, error };
3243
+ }
3249
3244
  try {
3250
- const userId = identity.requireUserId();
3251
- const table = tableFor(token, info.schema, info.table);
3252
- if (!table.ok) return table;
3253
- const row = {
3254
- [gate.titleColumn]: title,
3255
- ...info.ownerColumn ? { [info.ownerColumn]: userId } : {},
3256
- ...info.orgColumn && args.orgId ? { [info.orgColumn]: args.orgId } : {},
3257
- ...args.extraColumns ?? {}
3258
- };
3259
- const { data, error } = await table.query.insert(row).select("id").single();
3245
+ const { data, error } = await dataSource.rpc("entity_row_create", {
3246
+ p_token: token,
3247
+ p_title: title,
3248
+ p_organization_id: args.orgId
3249
+ });
3260
3250
  if (error) {
3261
3251
  errorSink({
3262
3252
  code: "entity_row_insert_failed",
3263
- message: `[createEntityRow] insert failed for "${token}": ${error.message}`,
3253
+ message: `[createEntityRow] entity_row_create failed for "${token}": ${error.message}`,
3264
3254
  context: { token, detail: error }
3265
3255
  });
3266
3256
  return { ok: false, error: error.message };
3267
3257
  }
3268
- const id = String(data.id);
3269
- titles.prime(token, id, title);
3270
- return { ok: true, data: { id, title } };
3258
+ const row = data;
3259
+ if (!row || typeof row.id !== "string" || !row.id) {
3260
+ const msg = "The create door returned no record.";
3261
+ errorSink({
3262
+ code: "entity_row_insert_failed",
3263
+ message: `[createEntityRow] entity_row_create returned no id for "${token}"`,
3264
+ context: { token, detail: data }
3265
+ });
3266
+ return { ok: false, error: msg };
3267
+ }
3268
+ const id = row.id;
3269
+ const named = typeof row.title === "string" && row.title ? row.title : title;
3270
+ titles.prime(token, id, named);
3271
+ return { ok: true, data: { id, title: named } };
3271
3272
  } catch (e) {
3272
3273
  const msg = e instanceof Error ? e.message : "Failed to create";
3273
3274
  errorSink({
@@ -3278,21 +3279,27 @@ function createEntityRowsService(deps, titles) {
3278
3279
  return { ok: false, error: msg };
3279
3280
  }
3280
3281
  },
3281
- /** Update the registry `titleColumn` of one row of `token`'s table. */
3282
+ /**
3283
+ * Rename one row of `token`'s table through `public.entity_row_rename`. The door
3284
+ * decides on the one ladder — the row's author, `editor` on the row, or a platform
3285
+ * admin — and answers "there is no such thing here" rather than telling a caller
3286
+ * that somebody else's row exists.
3287
+ */
3282
3288
  async renameEntityRow(token, id, title) {
3283
3289
  const gate = requireTitleColumn(token, "renaming rows");
3284
3290
  if (!gate.ok) return gate;
3285
- const info = registry.getEntityInfo(token);
3286
3291
  const trimmed = title.trim();
3287
3292
  if (!trimmed) return { ok: false, error: "Name is required" };
3288
3293
  try {
3289
- const table = tableFor(token, info.schema, info.table);
3290
- if (!table.ok) return table;
3291
- const { error } = await table.query.update({ [gate.titleColumn]: trimmed }).eq("id", id);
3294
+ const { error } = await dataSource.rpc("entity_row_rename", {
3295
+ p_token: token,
3296
+ p_id: id,
3297
+ p_title: trimmed
3298
+ });
3292
3299
  if (error) {
3293
3300
  errorSink({
3294
3301
  code: "entity_row_update_failed",
3295
- message: `[renameEntityRow] update failed for "${token}": ${error.message}`,
3302
+ message: `[renameEntityRow] entity_row_rename failed for "${token}": ${error.message}`,
3296
3303
  context: { token, id, detail: error }
3297
3304
  });
3298
3305
  return { ok: false, error: error.message };