@rebasepro/common 0.20.0 → 0.21.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.
@@ -10,19 +10,6 @@ export type TableCategory = "rebase-internal" | "junction" | "user";
10
10
  export declare const REBASE_INTERNAL_SCHEMAS: readonly string[];
11
11
  /** Table-name prefixes that mark a table as Rebase-internal regardless of schema. */
12
12
  export declare const REBASE_INTERNAL_PREFIXES: readonly string[];
13
- /**
14
- * Synchronously classify a table based on naming conventions.
15
- *
16
- * @param tableName - The unqualified name of the table.
17
- * @param schemaName - The schema the table belongs to (e.g. `"public"`, `"rebase"`).
18
- * @returns `"rebase-internal"` when the table belongs to a reserved schema or
19
- * carries a reserved prefix; `"user"` otherwise.
20
- *
21
- * @remarks
22
- * Junction-table detection requires an async database query and is therefore
23
- * **not** handled by this function. Use {@link detectJunctionTables} to obtain
24
- * the set of junction tables, then reclassify as needed.
25
- */
26
13
  export declare function classifyTable(tableName: string, schemaName: string): TableCategory;
27
14
  /**
28
15
  * Convenience predicate that checks whether a table is Rebase-internal.
@@ -75,3 +75,49 @@ export declare function toCallbackError(error: unknown, stage: string, path: str
75
75
  * @param path The collection path, for `details.path`.
76
76
  */
77
77
  export declare function callbackRefusal(stage: string, path: string): RebaseApiError;
78
+ /**
79
+ * The collection a callback tier is about to be handed — or a refusal.
80
+ *
81
+ * Every callback props type declares `collection: CollectionConfig`,
82
+ * non-optional, and the documented global-callback examples dereference it
83
+ * (`if (collection.slug === "audit_log") return;`). The driver resolves that
84
+ * value from the registry, which answers `undefined` for a path it does not
85
+ * know, so the tiers used to receive it through a cast that quietly dropped the
86
+ * `| undefined`. A global `beforeSave` reading `collection.slug` then threw a
87
+ * `TypeError` that `toCallbackError` reported as a 400 `CALLBACK_REJECTED` —
88
+ * the author's own rule blamed for a value the framework failed to supply.
89
+ *
90
+ * Skipping the tier instead is not available. `afterRead` is documented as the
91
+ * place for "security-critical redaction (PII masking, row filtering) — no read
92
+ * path bypasses it", and a tier that silently does not run on the paths the
93
+ * registry cannot resolve is precisely such a bypass.
94
+ *
95
+ * So the contract is the third option: **a callback tier never runs without a
96
+ * collection**, because a path that has none is refused before one can. That
97
+ * costs nothing, because it is already true — every read and every write
98
+ * reaches the database through `getCollectionByPath` in the driver's collection
99
+ * helpers, which raises this same "not found" for the same paths. Asking here
100
+ * only asks earlier, while the answer is still a 404 about the request instead
101
+ * of a `TypeError` attributed to the application's hook.
102
+ *
103
+ * @param collection The collection the driver resolved, if it resolved one.
104
+ * @param path The collection path, for the message and `details`.
105
+ */
106
+ export declare function requireCallbackCollection<C>(collection: C | undefined, path: string): C;
107
+ /**
108
+ * The client a callback reads as `context.client` — or a refusal naming why
109
+ * there is none.
110
+ *
111
+ * A driver is constructed before the server client exists, and
112
+ * `initializeRebaseBackend` hands it the client afterwards. So a driver can
113
+ * run callbacks without one: constructed on its own, or missed by that
114
+ * injection, which is how a second database's callbacks once ran with
115
+ * `context.client === undefined` while the type said it was there.
116
+ *
117
+ * Called from a getter on the context rather than when the context is built,
118
+ * because most callbacks never touch `client` and must not fail for its
119
+ * absence. The one that does gets this sentence instead of "Cannot read
120
+ * properties of undefined". A 500, not the 400 `toCallbackError` makes of a
121
+ * plain throw: the callback is not at fault, the server's wiring is.
122
+ */
123
+ export declare function requireCallbackClient<C>(client: C | undefined): C;
@@ -21,3 +21,4 @@ export * from "./conditions.js";
21
21
  export * from "./pg-column-to-property.js";
22
22
  export * from "./string-column-length.js";
23
23
  export * from "./internal-tables.js";
24
+ export * from "./sql-rows.js";
@@ -103,6 +103,23 @@ export declare function resolveJunctionSpecs(collections: CollectionConfig[]): M
103
103
  * payload column anywhere is a second description that can disagree.
104
104
  */
105
105
  export declare function getJunctionCollectionConfig(spec: JunctionSpec): CollectionConfig;
106
+ /**
107
+ * The same synthetic collection, reached from a resolved relation rather than
108
+ * from a spec.
109
+ *
110
+ * The spec is built by walking every collection, which the schema planner does
111
+ * once at boot and no request path can afford. A read or a write already holds
112
+ * the relation, and a relation's `through` carries the table, both key columns
113
+ * and the payload — everything the config is made of. One builder underneath
114
+ * both, so the shape the planner emitted columns from is the shape the write
115
+ * path validates a `_pivot` against and the read path strips it with.
116
+ */
117
+ export declare function getJunctionConfigForRelation(through: {
118
+ table: string;
119
+ sourceColumn: string;
120
+ targetColumn: string;
121
+ properties: Properties;
122
+ }): CollectionConfig;
106
123
  /**
107
124
  * Whether a parent-rule expression keeps its meaning when moved inside the
108
125
  * junction's `EXISTS` subquery — and the re-scoped copy if it does.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Read the rows out of whatever a SQL driver actually returned.
3
+ *
4
+ * Two shapes reach this repository's stores through the same call, and which
5
+ * one arrives is a property of the driver rather than of the query:
6
+ * node-postgres hands back a `{ rows }` envelope, and the other paths — a bare
7
+ * `executeSql`, Drizzle's `db.execute()` on some builds — hand back an array.
8
+ * `@rebasepro/server`'s `SqlExec` type declares the array, so the envelope is
9
+ * off-contract every time it turns up, and it still turns up.
10
+ *
11
+ * Reading one shape only is how a store silently sees nothing: the rate limiter
12
+ * that did it counted every caller as being on their first request — a limiter
13
+ * that never limits, with no error anywhere to say so.
14
+ *
15
+ * Written inline it came to `result as unknown as { rows?: T[] } | T[]`, five
16
+ * times across three packages. That is the problem restated as an assertion: a
17
+ * union the caller has to re-test at runtime anyway, with `unknown` in the
18
+ * middle only because neither half overlaps what the signature promised. The
19
+ * `Array.isArray` below is that same test done once, and it *narrows* — so
20
+ * these branches are checked rather than claimed.
21
+ *
22
+ * The element type is the one claim that stays a claim: these are rows from
23
+ * hand-written SQL, and nothing at this layer can check a column list.
24
+ */
25
+ export declare function sqlRows<T = Record<string, unknown>>(result: unknown): T[];
26
+ /** The first row of {@link sqlRows}, or `undefined` when there were none. */
27
+ export declare function firstSqlRow<T = Record<string, unknown>>(result: unknown): T | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/common",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Rebase shared core — collection registry, data driver adapter and fluent query builder. No React dependency.",
5
5
  "keywords": [
6
6
  "rebase",
@@ -44,8 +44,8 @@
44
44
  "dependencies": {
45
45
  "fast-equals": "6.0.2",
46
46
  "json-logic-js": "^2.0.5",
47
- "@rebasepro/types": "0.20.0",
48
- "@rebasepro/utils": "0.20.0"
47
+ "@rebasepro/types": "0.21.0",
48
+ "@rebasepro/utils": "0.21.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@jest/globals": "^30.4.1",