@porulle/db 0.1.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.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @porulle/db
2
+
3
+ The database surface plugins use instead of importing Drizzle directly.
4
+
5
+ ```ts
6
+ import { defineTable, column, eq, and, sql } from "@porulle/db";
7
+ ```
8
+
9
+ ## Why this exists
10
+
11
+ If every plugin imported from `drizzle-orm` directly, the framework couldn't:
12
+ - Inject `organizationId` and `id` columns automatically
13
+ - Add the standard `createdAt` / `updatedAt` timestamps
14
+ - Generate the per-org index + composite unique constraints
15
+ - Hot-swap the underlying ORM if we ever needed to (today: Drizzle on PostgreSQL — tomorrow could be different)
16
+
17
+ `@porulle/db` is the thin contract that decouples plugins from the ORM. Drizzle stays as an implementation detail.
18
+
19
+ ## API
20
+
21
+ ### `defineTable(name, columns)`
22
+
23
+ Top-level table. Auto-injects: `id` (UUID PK), `organizationId` (text NOT NULL with org index), `createdAt` and `updatedAt` (timestamps).
24
+
25
+ ```ts
26
+ import { defineTable, column } from "@porulle/db";
27
+
28
+ export const giftCards = defineTable("gift_cards", {
29
+ code: column.text({ unique: true }),
30
+ balance: column.integer(),
31
+ status: column.text({ enum: ["active", "disabled"], default: "active" }),
32
+ });
33
+ ```
34
+
35
+ ### `column.*` builders
36
+
37
+ `column.text({ unique?, optional?, enum?, default? })`
38
+ `column.integer({ optional?, default? })`
39
+ `column.boolean({ default? })`
40
+ `column.timestamp({ optional? })`
41
+ `column.jsonb<T>({ optional?, default? })`
42
+ `column.uuid({ optional?, default? })` — for FKs to other org-scoped tables (child tables skip the auto-org column)
43
+
44
+ ### Re-exported Drizzle operators
45
+
46
+ For query construction without importing `drizzle-orm` directly:
47
+
48
+ ```ts
49
+ eq, ne, gt, gte, lt, lte, and, or, not, like, ilike, notLike,
50
+ inArray, notInArray, isNull, isNotNull, between, sql, desc, asc
51
+ ```
52
+
53
+ ## Conventions
54
+
55
+ - Every top-level table is org-scoped via the auto-injected `organizationId`. Cross-tenant queries are a closed surface — see [`SECURITY.md`](../../SECURITY.md).
56
+ - Child tables (FK to a parent that already carries `organizationId`) get `id` and `createdAt` only. The parent's org column protects the child rows.
57
+ - Drizzle hash drift across workspace copies has bitten us before — `@porulle/db` re-exports the operators so plugins inherit a single drizzle version.
58
+
59
+ ## See also
60
+
61
+ - [Plugin Contract](https://github.com/asyncdotengineering/porulle/blob/main/apps/docs/src/content/docs/extending/plugin-contract.mdx) — Drizzle-first rule + org-scoping requirement
62
+ - `packages/core/src/kernel/database/` — how the kernel composes the schema
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Column definition helpers for defineTable.
3
+ *
4
+ * These produce config objects that defineTable maps to Drizzle column builders.
5
+ * Plugin developers use these instead of importing from drizzle-orm/pg-core directly.
6
+ *
7
+ * import { defineTable, column } from "@porulle/db";
8
+ *
9
+ * export const giftCards = defineTable("gift_cards", {
10
+ * code: column.text({ unique: true }),
11
+ * balance: column.integer(),
12
+ * status: column.text({ enum: ["active", "disabled"], default: "active" }),
13
+ * });
14
+ */
15
+ export interface TextColumnDef {
16
+ readonly _type: "text";
17
+ unique?: boolean;
18
+ optional?: boolean;
19
+ enum?: readonly string[];
20
+ default?: string;
21
+ }
22
+ export interface IntegerColumnDef {
23
+ readonly _type: "integer";
24
+ unique?: boolean;
25
+ optional?: boolean;
26
+ default?: number;
27
+ }
28
+ export interface BooleanColumnDef {
29
+ readonly _type: "boolean";
30
+ optional?: boolean;
31
+ default?: boolean;
32
+ }
33
+ export interface UuidColumnDef {
34
+ readonly _type: "uuid";
35
+ optional?: boolean;
36
+ references?: unknown;
37
+ }
38
+ export interface TimestampColumnDef {
39
+ readonly _type: "timestamp";
40
+ optional?: boolean;
41
+ default?: "now";
42
+ }
43
+ export interface JsonColumnDef {
44
+ readonly _type: "json";
45
+ optional?: boolean;
46
+ default?: unknown;
47
+ }
48
+ export type ColumnDef = TextColumnDef | IntegerColumnDef | BooleanColumnDef | UuidColumnDef | TimestampColumnDef | JsonColumnDef;
49
+ export declare const column: {
50
+ text: (opts?: Omit<TextColumnDef, "_type">) => TextColumnDef;
51
+ integer: (opts?: Omit<IntegerColumnDef, "_type">) => IntegerColumnDef;
52
+ boolean: (opts?: Omit<BooleanColumnDef, "_type">) => BooleanColumnDef;
53
+ uuid: (opts?: Omit<UuidColumnDef, "_type">) => UuidColumnDef;
54
+ timestamp: (opts?: Omit<TimestampColumnDef, "_type">) => TimestampColumnDef;
55
+ json: (opts?: Omit<JsonColumnDef, "_type">) => JsonColumnDef;
56
+ };
57
+ //# sourceMappingURL=column.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column.d.ts","sourceRoot":"","sources":["../src/column.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,MAAM,SAAS,GACjB,aAAa,GACb,gBAAgB,GAChB,gBAAgB,GAChB,aAAa,GACb,kBAAkB,GAClB,aAAa,CAAC;AAElB,eAAO,MAAM,MAAM;kBACH,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,KAAG,aAAa;qBAKzC,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,KAAG,gBAAgB;qBAKlD,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,KAAG,gBAAgB;kBAKrD,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,KAAG,aAAa;uBAKvC,IAAI,CAAC,kBAAkB,EAAE,OAAO,CAAC,KAAG,kBAAkB;kBAK3D,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,KAAG,aAAa;CAI3D,CAAC"}
package/dist/column.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Column definition helpers for defineTable.
3
+ *
4
+ * These produce config objects that defineTable maps to Drizzle column builders.
5
+ * Plugin developers use these instead of importing from drizzle-orm/pg-core directly.
6
+ *
7
+ * import { defineTable, column } from "@porulle/db";
8
+ *
9
+ * export const giftCards = defineTable("gift_cards", {
10
+ * code: column.text({ unique: true }),
11
+ * balance: column.integer(),
12
+ * status: column.text({ enum: ["active", "disabled"], default: "active" }),
13
+ * });
14
+ */
15
+ export const column = {
16
+ text: (opts) => ({
17
+ _type: "text",
18
+ ...opts,
19
+ }),
20
+ integer: (opts) => ({
21
+ _type: "integer",
22
+ ...opts,
23
+ }),
24
+ boolean: (opts) => ({
25
+ _type: "boolean",
26
+ ...opts,
27
+ }),
28
+ uuid: (opts) => ({
29
+ _type: "uuid",
30
+ ...opts,
31
+ }),
32
+ timestamp: (opts) => ({
33
+ _type: "timestamp",
34
+ ...opts,
35
+ }),
36
+ json: (opts) => ({
37
+ _type: "json",
38
+ ...opts,
39
+ }),
40
+ };
41
+ //# sourceMappingURL=column.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"column.js","sourceRoot":"","sources":["../src/column.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAiDH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,CAAC,IAAmC,EAAiB,EAAE,CAAC,CAAC;QAC7D,KAAK,EAAE,MAAe;QACtB,GAAG,IAAI;KACR,CAAC;IAEF,OAAO,EAAE,CAAC,IAAsC,EAAoB,EAAE,CAAC,CAAC;QACtE,KAAK,EAAE,SAAkB;QACzB,GAAG,IAAI;KACR,CAAC;IAEF,OAAO,EAAE,CAAC,IAAsC,EAAoB,EAAE,CAAC,CAAC;QACtE,KAAK,EAAE,SAAkB;QACzB,GAAG,IAAI;KACR,CAAC;IAEF,IAAI,EAAE,CAAC,IAAmC,EAAiB,EAAE,CAAC,CAAC;QAC7D,KAAK,EAAE,MAAe;QACtB,GAAG,IAAI;KACR,CAAC;IAEF,SAAS,EAAE,CAAC,IAAwC,EAAsB,EAAE,CAAC,CAAC;QAC5E,KAAK,EAAE,WAAoB;QAC3B,GAAG,IAAI;KACR,CAAC;IAEF,IAAI,EAAE,CAAC,IAAmC,EAAiB,EAAE,CAAC,CAAC;QAC7D,KAAK,EAAE,MAAe;QACtB,GAAG,IAAI;KACR,CAAC;CACH,CAAC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * defineTable — table definition wrapper for UnifiedCommerce plugins.
3
+ *
4
+ * Wraps Drizzle's pgTable with auto-injected fields:
5
+ * - id (UUID primary key)
6
+ * - organizationId (text, NOT NULL) — on top-level tables only
7
+ * - createdAt, updatedAt (timestamps)
8
+ * - Org index + composite unique constraints
9
+ *
10
+ * Child tables (FK to org-scoped parent) get id + createdAt only.
11
+ *
12
+ * Usage:
13
+ * import { defineTable, column } from "@porulle/db";
14
+ *
15
+ * export const giftCards = defineTable("gift_cards", {
16
+ * code: column.text({ unique: true }),
17
+ * balance: column.integer(),
18
+ * });
19
+ */
20
+ import type { ColumnDef } from "./column.js";
21
+ /**
22
+ * Define a database table with auto-injected UC fields.
23
+ */
24
+ export declare function defineTable(name: string, columnDefs: Record<string, ColumnDef>, extraConfig?: (table: Record<string, any>) => Record<string, any>): import("drizzle-orm/pg-core").PgTableWithColumns<{
25
+ name: string;
26
+ schema: undefined;
27
+ columns: {
28
+ [x: string]: import("drizzle-orm/pg-core").PgColumn<{
29
+ name: any;
30
+ tableName: string;
31
+ dataType: any;
32
+ columnType: any;
33
+ data: any;
34
+ driverParam: any;
35
+ notNull: false;
36
+ hasDefault: false;
37
+ isPrimaryKey: false;
38
+ isAutoincrement: false;
39
+ hasRuntimeDefault: false;
40
+ enumValues: any;
41
+ baseColumn: never;
42
+ identity: undefined;
43
+ generated: undefined;
44
+ }, {}, {
45
+ [x: string]: any;
46
+ [x: number]: any;
47
+ [x: symbol]: any;
48
+ }>;
49
+ };
50
+ dialect: "pg";
51
+ }>;
52
+ //# sourceMappingURL=define-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-table.d.ts","sourceRoot":"","sources":["../src/define-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAcH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAgE7C;;GAEG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EAErC,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsElE"}
@@ -0,0 +1,156 @@
1
+ /**
2
+ * defineTable — table definition wrapper for UnifiedCommerce plugins.
3
+ *
4
+ * Wraps Drizzle's pgTable with auto-injected fields:
5
+ * - id (UUID primary key)
6
+ * - organizationId (text, NOT NULL) — on top-level tables only
7
+ * - createdAt, updatedAt (timestamps)
8
+ * - Org index + composite unique constraints
9
+ *
10
+ * Child tables (FK to org-scoped parent) get id + createdAt only.
11
+ *
12
+ * Usage:
13
+ * import { defineTable, column } from "@porulle/db";
14
+ *
15
+ * export const giftCards = defineTable("gift_cards", {
16
+ * code: column.text({ unique: true }),
17
+ * balance: column.integer(),
18
+ * });
19
+ */
20
+ import { pgTable, uuid, text, integer, boolean, timestamp, jsonb, index, uniqueIndex, } from "drizzle-orm/pg-core";
21
+ import { getTableColumns } from "drizzle-orm";
22
+ function hasOrgColumn(table) {
23
+ if (!table || typeof table !== "object")
24
+ return false;
25
+ try {
26
+ const cols = getTableColumns(table);
27
+ return "organizationId" in cols;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ function toSnake(name) {
34
+ return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
35
+ }
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ function mapColumn(name, def) {
38
+ const sn = toSnake(name);
39
+ switch (def._type) {
40
+ case "text": {
41
+ let c = def.enum ? text(sn, { enum: def.enum }) : text(sn);
42
+ if (def.default !== undefined)
43
+ c = c.default(def.default);
44
+ if (!def.optional)
45
+ c = c.notNull();
46
+ return c;
47
+ }
48
+ case "integer": {
49
+ let c = integer(sn);
50
+ if (def.default !== undefined)
51
+ c = c.default(def.default);
52
+ if (!def.optional)
53
+ c = c.notNull();
54
+ return c;
55
+ }
56
+ case "boolean": {
57
+ let c = boolean(sn);
58
+ if (def.default !== undefined)
59
+ c = c.default(def.default);
60
+ if (!def.optional)
61
+ c = c.notNull();
62
+ return c;
63
+ }
64
+ case "uuid": {
65
+ let c = uuid(sn);
66
+ if (def.references) {
67
+ const refCols = getTableColumns(def.references);
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ c = c.references(() => refCols.id, { onDelete: "cascade" });
70
+ }
71
+ if (!def.optional)
72
+ c = c.notNull();
73
+ return c;
74
+ }
75
+ case "timestamp": {
76
+ let c = timestamp(sn, { withTimezone: true });
77
+ if (def.default === "now")
78
+ c = c.defaultNow();
79
+ if (!def.optional)
80
+ c = c.notNull();
81
+ return c;
82
+ }
83
+ case "json": {
84
+ let c = jsonb(sn);
85
+ if (def.default !== undefined)
86
+ c = c.default(def.default);
87
+ if (!def.optional)
88
+ c = c.notNull();
89
+ return c;
90
+ }
91
+ }
92
+ }
93
+ /**
94
+ * Define a database table with auto-injected UC fields.
95
+ */
96
+ export function defineTable(name, columnDefs,
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ extraConfig) {
99
+ // Detect child table: any UUID column references an org-scoped parent?
100
+ let isChild = false;
101
+ for (const def of Object.values(columnDefs)) {
102
+ if (def._type === "uuid" && def.references && hasOrgColumn(def.references)) {
103
+ isChild = true;
104
+ break;
105
+ }
106
+ }
107
+ // Map user columns to Drizzle builders
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ const cols = {};
110
+ const uniqueCols = [];
111
+ for (const [colName, def] of Object.entries(columnDefs)) {
112
+ cols[colName] = mapColumn(colName, def);
113
+ if ("unique" in def && def.unique) {
114
+ uniqueCols.push(colName);
115
+ }
116
+ }
117
+ // Build full column set with auto-injected fields
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ const allColumns = {
120
+ id: uuid("id").defaultRandom().primaryKey(),
121
+ };
122
+ if (!isChild) {
123
+ allColumns.organizationId = text("organization_id").notNull();
124
+ }
125
+ Object.assign(allColumns, cols);
126
+ allColumns.createdAt = timestamp("created_at", { withTimezone: true })
127
+ .defaultNow()
128
+ .notNull();
129
+ if (!isChild) {
130
+ allColumns.updatedAt = timestamp("updated_at", { withTimezone: true })
131
+ .defaultNow()
132
+ .notNull();
133
+ }
134
+ // Create pgTable with indexes
135
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
136
+ const table = pgTable(name, allColumns, (t) => {
137
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
138
+ const indexes = {};
139
+ if (!isChild) {
140
+ indexes.orgIdx = index(`idx_${name}_org`).on(t.organizationId);
141
+ for (const colName of uniqueCols) {
142
+ const sn = toSnake(colName);
143
+ indexes[`${colName}Unique`] = uniqueIndex(`${name}_org_${sn}_unique`)
144
+ .on(t.organizationId, t[colName]);
145
+ }
146
+ }
147
+ if (extraConfig)
148
+ Object.assign(indexes, extraConfig(t));
149
+ return indexes;
150
+ });
151
+ // Mark for scoped DB proxy
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ table.__ucOrgScoped = !isChild;
154
+ return table;
155
+ }
156
+ //# sourceMappingURL=define-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-table.js","sourceRoot":"","sources":["../src/define-table.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @porulle/db — single import for all database work.
3
+ *
4
+ * Plugin developers import everything from here instead of drizzle-orm directly.
5
+ * Drizzle is an implementation detail — this package controls the surface.
6
+ *
7
+ * import { defineTable, column, eq, and, desc, sql } from "@porulle/db";
8
+ */
9
+ export { defineTable } from "./define-table.js";
10
+ export { column } from "./column.js";
11
+ export type { ColumnDef } from "./column.js";
12
+ export { eq, ne, gt, gte, lt, lte, and, or, not, like, ilike, notLike, inArray, notInArray, between, notBetween, isNull, isNotNull, exists, notExists, sql, asc, desc, count, sum, avg, min, max, countDistinct, sumDistinct, avgDistinct, getTableColumns, } from "drizzle-orm";
13
+ export { pgTable, index, uniqueIndex, check, } from "drizzle-orm/pg-core";
14
+ export type { PgTable, PgDatabase, PgQueryResultHKT, } from "drizzle-orm/pg-core";
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAG7C,OAAO,EACL,EAAE,EACF,EAAE,EACF,EAAE,EACF,GAAG,EACH,EAAE,EACF,GAAG,EACH,GAAG,EACH,EAAE,EACF,GAAG,EACH,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,UAAU,EACV,OAAO,EACP,UAAU,EACV,MAAM,EACN,SAAS,EACT,MAAM,EACN,SAAS,EACT,GAAG,EACH,GAAG,EACH,IAAI,EACJ,KAAK,EACL,GAAG,EACH,GAAG,EACH,GAAG,EACH,GAAG,EACH,aAAa,EACb,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,OAAO,EACP,KAAK,EACL,WAAW,EACX,KAAK,GACN,MAAM,qBAAqB,CAAC;AAG7B,YAAY,EACV,OAAO,EACP,UAAU,EACV,gBAAgB,GACjB,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @porulle/db — single import for all database work.
3
+ *
4
+ * Plugin developers import everything from here instead of drizzle-orm directly.
5
+ * Drizzle is an implementation detail — this package controls the surface.
6
+ *
7
+ * import { defineTable, column, eq, and, desc, sql } from "@porulle/db";
8
+ */
9
+ // ─── UC Abstractions ──────────────────────────────────────────────────
10
+ export { defineTable } from "./define-table.js";
11
+ export { column } from "./column.js";
12
+ // ─── Drizzle Query Operators (re-exported) ────────────────────────────
13
+ export { eq, ne, gt, gte, lt, lte, and, or, not, like, ilike, notLike, inArray, notInArray, between, notBetween, isNull, isNotNull, exists, notExists, sql, asc, desc, count, sum, avg, min, max, countDistinct, sumDistinct, avgDistinct, getTableColumns, } from "drizzle-orm";
14
+ // ─── Drizzle PG-Specific (re-exported) ───────────────────────────────
15
+ export { pgTable, index, uniqueIndex, check, } from "drizzle-orm/pg-core";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,yEAAyE;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,yEAAyE;AACzE,OAAO,EACL,EAAE,EACF,EAAE,EACF,EAAE,EACF,GAAG,EACH,EAAE,EACF,GAAG,EACH,GAAG,EACH,EAAE,EACF,GAAG,EACH,IAAI,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EACP,UAAU,EACV,OAAO,EACP,UAAU,EACV,MAAM,EACN,SAAS,EACT,MAAM,EACN,SAAS,EACT,GAAG,EACH,GAAG,EACH,IAAI,EACJ,KAAK,EACL,GAAG,EACH,GAAG,EACH,GAAG,EACH,GAAG,EACH,aAAa,EACb,WAAW,EACX,WAAW,EACX,eAAe,GAChB,MAAM,aAAa,CAAC;AAErB,wEAAwE;AACxE,OAAO,EACL,OAAO,EACP,KAAK,EACL,WAAW,EACX,KAAK,GACN,MAAM,qBAAqB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@porulle/db",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "bun": "./src/index.ts",
9
+ "import": "./dist/index.js",
10
+ "types": "./src/index.ts"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "check-types": "tsc --noEmit",
15
+ "test": "vitest run",
16
+ "backfill-order-seq": "bun run ./scripts/backfill-order-seq.ts",
17
+ "backfill-inventory-org-id": "bun run ./scripts/backfill-inventory-org-id.ts",
18
+ "build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json"
19
+ },
20
+ "dependencies": {
21
+ "@porulle/core": "workspace:*",
22
+ "drizzle-orm": "^0.45.1",
23
+ "postgres": "^3.4.7"
24
+ },
25
+ "devDependencies": {
26
+ "@repo/typescript-config": "*",
27
+ "@types/node": "^24.5.2",
28
+ "typescript": "5.9.2",
29
+ "vitest": "^3.2.4"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "files": [
35
+ "src",
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "description": "The database surface plugins use instead of importing Drizzle directly.",
40
+ "homepage": "https://porulle-docs.vercel.app",
41
+ "bugs": {
42
+ "url": "https://github.com/asyncdotengineering/porulle/issues"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/asyncdotengineering/porulle.git",
47
+ "directory": "packages/db"
48
+ },
49
+ "author": "Porulle contributors"
50
+ }
package/src/column.ts ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Column definition helpers for defineTable.
3
+ *
4
+ * These produce config objects that defineTable maps to Drizzle column builders.
5
+ * Plugin developers use these instead of importing from drizzle-orm/pg-core directly.
6
+ *
7
+ * import { defineTable, column } from "@porulle/db";
8
+ *
9
+ * export const giftCards = defineTable("gift_cards", {
10
+ * code: column.text({ unique: true }),
11
+ * balance: column.integer(),
12
+ * status: column.text({ enum: ["active", "disabled"], default: "active" }),
13
+ * });
14
+ */
15
+
16
+ export interface TextColumnDef {
17
+ readonly _type: "text";
18
+ unique?: boolean;
19
+ optional?: boolean;
20
+ enum?: readonly string[];
21
+ default?: string;
22
+ }
23
+
24
+ export interface IntegerColumnDef {
25
+ readonly _type: "integer";
26
+ unique?: boolean;
27
+ optional?: boolean;
28
+ default?: number;
29
+ }
30
+
31
+ export interface BooleanColumnDef {
32
+ readonly _type: "boolean";
33
+ optional?: boolean;
34
+ default?: boolean;
35
+ }
36
+
37
+ export interface UuidColumnDef {
38
+ readonly _type: "uuid";
39
+ optional?: boolean;
40
+ references?: unknown; // PgTable reference for FK detection
41
+ }
42
+
43
+ export interface TimestampColumnDef {
44
+ readonly _type: "timestamp";
45
+ optional?: boolean;
46
+ default?: "now";
47
+ }
48
+
49
+ export interface JsonColumnDef {
50
+ readonly _type: "json";
51
+ optional?: boolean;
52
+ default?: unknown;
53
+ }
54
+
55
+ export type ColumnDef =
56
+ | TextColumnDef
57
+ | IntegerColumnDef
58
+ | BooleanColumnDef
59
+ | UuidColumnDef
60
+ | TimestampColumnDef
61
+ | JsonColumnDef;
62
+
63
+ export const column = {
64
+ text: (opts?: Omit<TextColumnDef, "_type">): TextColumnDef => ({
65
+ _type: "text" as const,
66
+ ...opts,
67
+ }),
68
+
69
+ integer: (opts?: Omit<IntegerColumnDef, "_type">): IntegerColumnDef => ({
70
+ _type: "integer" as const,
71
+ ...opts,
72
+ }),
73
+
74
+ boolean: (opts?: Omit<BooleanColumnDef, "_type">): BooleanColumnDef => ({
75
+ _type: "boolean" as const,
76
+ ...opts,
77
+ }),
78
+
79
+ uuid: (opts?: Omit<UuidColumnDef, "_type">): UuidColumnDef => ({
80
+ _type: "uuid" as const,
81
+ ...opts,
82
+ }),
83
+
84
+ timestamp: (opts?: Omit<TimestampColumnDef, "_type">): TimestampColumnDef => ({
85
+ _type: "timestamp" as const,
86
+ ...opts,
87
+ }),
88
+
89
+ json: (opts?: Omit<JsonColumnDef, "_type">): JsonColumnDef => ({
90
+ _type: "json" as const,
91
+ ...opts,
92
+ }),
93
+ };
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { getTableColumns } from "drizzle-orm";
3
+ import { defineTable } from "./define-table.js";
4
+ import { column } from "./column.js";
5
+
6
+ describe("defineTable", () => {
7
+ it("auto-injects id, organizationId, createdAt, updatedAt on top-level tables", () => {
8
+ const table = defineTable("test_products", {
9
+ name: column.text(),
10
+ price: column.integer(),
11
+ });
12
+
13
+ const cols = getTableColumns(table);
14
+ expect(cols).toHaveProperty("id");
15
+ expect(cols).toHaveProperty("organizationId");
16
+ expect(cols).toHaveProperty("createdAt");
17
+ expect(cols).toHaveProperty("updatedAt");
18
+ expect(cols).toHaveProperty("name");
19
+ expect(cols).toHaveProperty("price");
20
+ });
21
+
22
+ it("marks top-level tables as __ucOrgScoped", () => {
23
+ const table = defineTable("test_items", {
24
+ title: column.text(),
25
+ });
26
+ expect((table as Record<string, unknown>).__ucOrgScoped).toBe(true);
27
+ });
28
+
29
+ it("detects child tables (FK to org-scoped parent) and skips organizationId", () => {
30
+ const parent = defineTable("test_parent", {
31
+ slug: column.text({ unique: true }),
32
+ });
33
+
34
+ const child = defineTable("test_child", {
35
+ parentId: column.uuid({ references: parent }),
36
+ note: column.text({ optional: true }),
37
+ });
38
+
39
+ const parentCols = getTableColumns(parent);
40
+ const childCols = getTableColumns(child);
41
+
42
+ // Parent has organizationId
43
+ expect(parentCols).toHaveProperty("organizationId");
44
+ // Child does NOT
45
+ expect(childCols).not.toHaveProperty("organizationId");
46
+ // Child has id + createdAt but no updatedAt
47
+ expect(childCols).toHaveProperty("id");
48
+ expect(childCols).toHaveProperty("createdAt");
49
+ expect(childCols).not.toHaveProperty("updatedAt");
50
+ // Child is not org-scoped
51
+ expect((child as Record<string, unknown>).__ucOrgScoped).toBe(false);
52
+ });
53
+
54
+ it("supports all column types", () => {
55
+ const table = defineTable("test_all_types", {
56
+ name: column.text(),
57
+ count: column.integer({ default: 0 }),
58
+ active: column.boolean({ default: true }),
59
+ data: column.json({ default: {} }),
60
+ happenedAt: column.timestamp({ optional: true }),
61
+ });
62
+
63
+ const cols = getTableColumns(table);
64
+ expect(cols).toHaveProperty("name");
65
+ expect(cols).toHaveProperty("count");
66
+ expect(cols).toHaveProperty("active");
67
+ expect(cols).toHaveProperty("data");
68
+ expect(cols).toHaveProperty("happenedAt");
69
+ });
70
+
71
+ it("supports enum columns", () => {
72
+ const table = defineTable("test_enum", {
73
+ status: column.text({ enum: ["active", "disabled", "exhausted"], default: "active" }),
74
+ });
75
+
76
+ const cols = getTableColumns(table);
77
+ expect(cols).toHaveProperty("status");
78
+ });
79
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * defineTable — table definition wrapper for UnifiedCommerce plugins.
3
+ *
4
+ * Wraps Drizzle's pgTable with auto-injected fields:
5
+ * - id (UUID primary key)
6
+ * - organizationId (text, NOT NULL) — on top-level tables only
7
+ * - createdAt, updatedAt (timestamps)
8
+ * - Org index + composite unique constraints
9
+ *
10
+ * Child tables (FK to org-scoped parent) get id + createdAt only.
11
+ *
12
+ * Usage:
13
+ * import { defineTable, column } from "@porulle/db";
14
+ *
15
+ * export const giftCards = defineTable("gift_cards", {
16
+ * code: column.text({ unique: true }),
17
+ * balance: column.integer(),
18
+ * });
19
+ */
20
+
21
+ import {
22
+ pgTable,
23
+ uuid,
24
+ text,
25
+ integer,
26
+ boolean,
27
+ timestamp,
28
+ jsonb,
29
+ index,
30
+ uniqueIndex,
31
+ } from "drizzle-orm/pg-core";
32
+ import { getTableColumns } from "drizzle-orm";
33
+ import type { ColumnDef } from "./column.js";
34
+
35
+ function hasOrgColumn(table: unknown): boolean {
36
+ if (!table || typeof table !== "object") return false;
37
+ try {
38
+ const cols = getTableColumns(table as Parameters<typeof getTableColumns>[0]);
39
+ return "organizationId" in cols;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function toSnake(name: string): string {
46
+ return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
47
+ }
48
+
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ function mapColumn(name: string, def: ColumnDef): any {
51
+ const sn = toSnake(name);
52
+
53
+ switch (def._type) {
54
+ case "text": {
55
+ let c = def.enum ? text(sn, { enum: def.enum as [string, ...string[]] }) : text(sn);
56
+ if (def.default !== undefined) c = c.default(def.default) as typeof c;
57
+ if (!def.optional) c = c.notNull() as typeof c;
58
+ return c;
59
+ }
60
+ case "integer": {
61
+ let c = integer(sn);
62
+ if (def.default !== undefined) c = c.default(def.default) as typeof c;
63
+ if (!def.optional) c = c.notNull() as typeof c;
64
+ return c;
65
+ }
66
+ case "boolean": {
67
+ let c = boolean(sn);
68
+ if (def.default !== undefined) c = c.default(def.default) as typeof c;
69
+ if (!def.optional) c = c.notNull() as typeof c;
70
+ return c;
71
+ }
72
+ case "uuid": {
73
+ let c = uuid(sn);
74
+ if (def.references) {
75
+ const refCols = getTableColumns(def.references as Parameters<typeof getTableColumns>[0]);
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ c = c.references(() => (refCols as any).id, { onDelete: "cascade" }) as typeof c;
78
+ }
79
+ if (!def.optional) c = c.notNull() as typeof c;
80
+ return c;
81
+ }
82
+ case "timestamp": {
83
+ let c = timestamp(sn, { withTimezone: true });
84
+ if (def.default === "now") c = c.defaultNow() as typeof c;
85
+ if (!def.optional) c = c.notNull() as typeof c;
86
+ return c;
87
+ }
88
+ case "json": {
89
+ let c = jsonb(sn);
90
+ if (def.default !== undefined) c = c.default(def.default) as typeof c;
91
+ if (!def.optional) c = c.notNull() as typeof c;
92
+ return c;
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Define a database table with auto-injected UC fields.
99
+ */
100
+ export function defineTable(
101
+ name: string,
102
+ columnDefs: Record<string, ColumnDef>,
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ extraConfig?: (table: Record<string, any>) => Record<string, any>,
105
+ ) {
106
+ // Detect child table: any UUID column references an org-scoped parent?
107
+ let isChild = false;
108
+ for (const def of Object.values(columnDefs)) {
109
+ if (def._type === "uuid" && def.references && hasOrgColumn(def.references)) {
110
+ isChild = true;
111
+ break;
112
+ }
113
+ }
114
+
115
+ // Map user columns to Drizzle builders
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ const cols: Record<string, any> = {};
118
+ const uniqueCols: string[] = [];
119
+
120
+ for (const [colName, def] of Object.entries(columnDefs)) {
121
+ cols[colName] = mapColumn(colName, def);
122
+ if ("unique" in def && def.unique) {
123
+ uniqueCols.push(colName);
124
+ }
125
+ }
126
+
127
+ // Build full column set with auto-injected fields
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ const allColumns: Record<string, any> = {
130
+ id: uuid("id").defaultRandom().primaryKey(),
131
+ };
132
+
133
+ if (!isChild) {
134
+ allColumns.organizationId = text("organization_id").notNull();
135
+ }
136
+
137
+ Object.assign(allColumns, cols);
138
+
139
+ allColumns.createdAt = timestamp("created_at", { withTimezone: true })
140
+ .defaultNow()
141
+ .notNull();
142
+
143
+ if (!isChild) {
144
+ allColumns.updatedAt = timestamp("updated_at", { withTimezone: true })
145
+ .defaultNow()
146
+ .notNull();
147
+ }
148
+
149
+ // Create pgTable with indexes
150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
+ const table = pgTable(name, allColumns as any, (t: any) => {
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ const indexes: Record<string, any> = {};
154
+
155
+ if (!isChild) {
156
+ indexes.orgIdx = index(`idx_${name}_org`).on(t.organizationId);
157
+
158
+ for (const colName of uniqueCols) {
159
+ const sn = toSnake(colName);
160
+ indexes[`${colName}Unique`] = uniqueIndex(`${name}_org_${sn}_unique`)
161
+ .on(t.organizationId, t[colName]);
162
+ }
163
+ }
164
+
165
+ if (extraConfig) Object.assign(indexes, extraConfig(t));
166
+ return indexes;
167
+ });
168
+
169
+ // Mark for scoped DB proxy
170
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
+ (table as any).__ucOrgScoped = !isChild;
172
+
173
+ return table;
174
+ }
package/src/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @porulle/db — single import for all database work.
3
+ *
4
+ * Plugin developers import everything from here instead of drizzle-orm directly.
5
+ * Drizzle is an implementation detail — this package controls the surface.
6
+ *
7
+ * import { defineTable, column, eq, and, desc, sql } from "@porulle/db";
8
+ */
9
+
10
+ // ─── UC Abstractions ──────────────────────────────────────────────────
11
+ export { defineTable } from "./define-table.js";
12
+ export { column } from "./column.js";
13
+ export type { ColumnDef } from "./column.js";
14
+
15
+ // ─── Drizzle Query Operators (re-exported) ────────────────────────────
16
+ export {
17
+ eq,
18
+ ne,
19
+ gt,
20
+ gte,
21
+ lt,
22
+ lte,
23
+ and,
24
+ or,
25
+ not,
26
+ like,
27
+ ilike,
28
+ notLike,
29
+ inArray,
30
+ notInArray,
31
+ between,
32
+ notBetween,
33
+ isNull,
34
+ isNotNull,
35
+ exists,
36
+ notExists,
37
+ sql,
38
+ asc,
39
+ desc,
40
+ count,
41
+ sum,
42
+ avg,
43
+ min,
44
+ max,
45
+ countDistinct,
46
+ sumDistinct,
47
+ avgDistinct,
48
+ getTableColumns,
49
+ } from "drizzle-orm";
50
+
51
+ // ─── Drizzle PG-Specific (re-exported) ───────────────────────────────
52
+ export {
53
+ pgTable,
54
+ index,
55
+ uniqueIndex,
56
+ check,
57
+ } from "drizzle-orm/pg-core";
58
+
59
+ // ─── Drizzle Types (re-exported for advanced use) ─────────────────────
60
+ export type {
61
+ PgTable,
62
+ PgDatabase,
63
+ PgQueryResultHKT,
64
+ } from "drizzle-orm/pg-core";