@quicktalog/common 1.52.0 → 1.54.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,113 @@
1
+ # @quicktalog/common
2
+
3
+ Shared TypeScript building blocks for the Quicktalog applications — published to npm and installed
4
+ by every service that needs to agree on the same types, constants, helpers, and database schema.
5
+
6
+ The goal is a single source of truth. When a catalogue gains a field or a pricing tier changes, it
7
+ changes here once, gets published, and every consumer picks it up by bumping a version rather than
8
+ re-declaring the shape locally and drifting out of sync.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @quicktalog/common
14
+ ```
15
+
16
+ ```ts
17
+ import { tiers, defaultCatalogueData, generateUniqueSlug, schema } from "@quicktalog/common";
18
+ ```
19
+
20
+ Everything is re-exported from the package root, so there are no deep import paths to remember.
21
+
22
+ ## What's inside
23
+
24
+ The package is ESM-only (`"type": "module"`) and ships compiled JavaScript plus `.d.ts`
25
+ declarations from `dist/`. Source lives in `src/` and is organised into four groups:
26
+
27
+ | Group | Path | Contents |
28
+ | --- | --- | --- |
29
+ | **Types** | `src/types/` | Domain models and unions — `Catalogue`, `ContentBlockType`, `Status`, `ThemeType`, `LimitType`, and friends |
30
+ | **Constants** | `src/constants/` | Shared values — `themes`, `layouts`, `tiers`, `BUSINESS_TYPES`, `defaultCatalogueData` |
31
+ | **Helpers** | `src/helpers/` | Small utilities such as `generateUniqueSlug` and `fetchImageFromUnsplash` |
32
+ | **Drizzle** | `src/drizzle/` | The Postgres schema, exported as a `schema` namespace, plus generated relations |
33
+
34
+ The Drizzle schema is **generated, not hand-written**. `src/drizzle/migrations/schema.ts` is
35
+ produced by `drizzle-kit pull`, which introspects the live Postgres database described by
36
+ `drizzle.config.ts`. Treat the database as authoritative and regenerate rather than editing those
37
+ files directly.
38
+
39
+ ## Local development
40
+
41
+ ```bash
42
+ npm install # also installs the git hooks via the prepare script
43
+ npm run build # tsc -> dist/
44
+ ```
45
+
46
+ ### Git hooks
47
+
48
+ A single Husky hook runs on commit:
49
+
50
+ ```
51
+ .husky/pre-commit
52
+ ├── npx drizzle-kit pull (only when a real DATABASE_URL is available)
53
+ └── npm run build
54
+ ```
55
+
56
+ The drizzle step is guarded. It looks for `DATABASE_URL` in the environment, falls back to `.env`,
57
+ and runs only if the value actually looks like a connection string. Without one it prints a skip
58
+ notice and moves on, so fresh clones and CI can commit without a database. The build always runs,
59
+ so a commit that does not typecheck cannot be created.
60
+
61
+ There is deliberately **no pre-push hook**. Versioning is explicit — see below.
62
+
63
+ > **Note:** when the drizzle step does run, it rewrites files under `src/drizzle/migrations/`
64
+ > *after* staging has already happened. Those regenerated files land in your working tree unstaged;
65
+ > review and commit them yourself.
66
+
67
+ ## Release workflow
68
+
69
+ Publishing is driven entirely by the version in `package.json`. Pushing to `main` is what triggers
70
+ CI, but only a **new** version actually publishes.
71
+
72
+ ```
73
+ npm run release
74
+ ├── npm version minor -> bumps package.json, commits it, tags it
75
+ │ (the commit message is just the version, e.g. "1.53.0")
76
+ └── git push --follow-tags -> pushes the commit and its tag
77
+ ```
78
+
79
+ Use `npm run release:patch` or `npm run release:major` for the other bump types. `--follow-tags`
80
+ matters: a plain `git push` leaves the tag stranded locally.
81
+
82
+ Because the bump lives *inside* the commit being pushed, the version CI sees is always the version
83
+ you intended to ship. The commit message being the bare version string is also why the GitHub
84
+ Actions run shows up named after the release.
85
+
86
+ ### What CI does
87
+
88
+ `.github/workflows/publish.yaml` runs on every push to `main`:
89
+
90
+ 1. Checkout, set up Node 22, upgrade npm for OIDC support
91
+ 2. `npm ci`
92
+ 3. `npm run build`
93
+ 4. **Check the registry** for the current `name@version`
94
+ 5. `npm publish --access public` — only if that version does not already exist
95
+
96
+ Step 4 is what keeps ordinary commits green. A docs or config push builds and verifies as usual,
97
+ then skips publishing instead of failing on a version that is already taken. The job summary states
98
+ which path it took, so a green run never leaves you guessing whether it shipped.
99
+
100
+ ### Authentication
101
+
102
+ There is no npm token. Publishing uses **OIDC trusted publishing** — GitHub Actions proves its
103
+ identity to npm directly, and npm attaches a signed [provenance](https://docs.npmjs.com/generating-provenance-statements)
104
+ statement to each release.
105
+
106
+ This is why `package.json` must carry a `repository` field pointing at this repo: npm validates the
107
+ provenance bundle against it and rejects the publish with `E422` if the two disagree.
108
+
109
+ ## Conventions
110
+
111
+ - **Never edit `package.json` version by hand** — use the release scripts so the tag and commit stay consistent
112
+ - **Never hand-edit the generated Drizzle schema** — change the database and re-pull
113
+ - `dist/` is gitignored and built by CI; only `src/` is tracked
@@ -28,6 +28,7 @@ export const tiers = [
28
28
  apperance: {
29
29
  standardThemes: true,
30
30
  styles: false,
31
+ customThemes: false,
31
32
  },
32
33
  },
33
34
  },
@@ -60,6 +61,7 @@ export const tiers = [
60
61
  apperance: {
61
62
  standardThemes: true,
62
63
  styles: false,
64
+ customThemes: false,
63
65
  },
64
66
  },
65
67
  },
@@ -92,6 +94,7 @@ export const tiers = [
92
94
  apperance: {
93
95
  standardThemes: true,
94
96
  styles: true,
97
+ customThemes: true,
95
98
  },
96
99
  },
97
100
  },
@@ -124,6 +127,7 @@ export const tiers = [
124
127
  apperance: {
125
128
  standardThemes: true,
126
129
  styles: true,
130
+ customThemes: true,
127
131
  },
128
132
  },
129
133
  },
@@ -156,6 +160,7 @@ export const tiers = [
156
160
  apperance: {
157
161
  standardThemes: true,
158
162
  styles: true,
163
+ customThemes: true,
159
164
  },
160
165
  },
161
166
  },
@@ -188,6 +193,7 @@ export const tiers = [
188
193
  apperance: {
189
194
  standardThemes: true,
190
195
  styles: true,
196
+ customThemes: true,
191
197
  },
192
198
  },
193
199
  },
@@ -14,6 +14,7 @@ export declare const usersRelations: import("drizzle-orm/relations").Relations<"
14
14
  ocrs: import("drizzle-orm/relations").Many<"ocr">;
15
15
  analytics: import("drizzle-orm/relations").Many<"analytics">;
16
16
  catalogues: import("drizzle-orm/relations").Many<"catalogues">;
17
+ userThemes: import("drizzle-orm/relations").Many<"user_themes">;
17
18
  }>;
18
19
  export declare const newsletterRelations: import("drizzle-orm/relations").Relations<"newsletter", {
19
20
  user: import("drizzle-orm/relations").One<"users", true>;
@@ -28,3 +29,6 @@ export declare const analyticsRelations: import("drizzle-orm/relations").Relatio
28
29
  export declare const qrConfigsRelations: import("drizzle-orm/relations").Relations<"qr_configs", {
29
30
  catalogue: import("drizzle-orm/relations").One<"catalogues", true>;
30
31
  }>;
32
+ export declare const userThemesRelations: import("drizzle-orm/relations").Relations<"user_themes", {
33
+ user: import("drizzle-orm/relations").One<"users", true>;
34
+ }>;
@@ -1,5 +1,5 @@
1
1
  import { relations } from "drizzle-orm/relations";
2
- import { catalogues, prompts, users, newsletter, ocr, analytics, qrConfigs } from "./schema";
2
+ import { catalogues, prompts, users, newsletter, ocr, analytics, qrConfigs, userThemes } from "./schema";
3
3
  export const promptsRelations = relations(prompts, ({ one }) => ({
4
4
  catalogue: one(catalogues, {
5
5
  fields: [prompts.catalogue],
@@ -25,6 +25,7 @@ export const usersRelations = relations(users, ({ many }) => ({
25
25
  ocrs: many(ocr),
26
26
  analytics: many(analytics),
27
27
  catalogues: many(catalogues),
28
+ userThemes: many(userThemes),
28
29
  }));
29
30
  export const newsletterRelations = relations(newsletter, ({ one }) => ({
30
31
  user: one(users, {
@@ -54,3 +55,9 @@ export const qrConfigsRelations = relations(qrConfigs, ({ one }) => ({
54
55
  references: [catalogues.name]
55
56
  }),
56
57
  }));
58
+ export const userThemesRelations = relations(userThemes, ({ one }) => ({
59
+ user: one(users, {
60
+ fields: [userThemes.userId],
61
+ references: [users.id]
62
+ }),
63
+ }));
@@ -911,6 +911,115 @@ export declare const qrConfigs: import("drizzle-orm/pg-core").PgTableWithColumns
911
911
  };
912
912
  dialect: "pg";
913
913
  }>;
914
+ export declare const userThemes: import("drizzle-orm/pg-core").PgTableWithColumns<{
915
+ name: "user_themes";
916
+ schema: undefined;
917
+ columns: {
918
+ id: import("drizzle-orm/pg-core").PgColumn<{
919
+ name: "id";
920
+ tableName: "user_themes";
921
+ dataType: "string";
922
+ columnType: "PgUUID";
923
+ data: string;
924
+ driverParam: string;
925
+ notNull: true;
926
+ hasDefault: true;
927
+ isPrimaryKey: true;
928
+ isAutoincrement: false;
929
+ hasRuntimeDefault: false;
930
+ enumValues: undefined;
931
+ baseColumn: never;
932
+ identity: undefined;
933
+ generated: undefined;
934
+ }, {}, {}>;
935
+ userId: import("drizzle-orm/pg-core").PgColumn<{
936
+ name: "user_id";
937
+ tableName: "user_themes";
938
+ dataType: "string";
939
+ columnType: "PgText";
940
+ data: string;
941
+ driverParam: string;
942
+ notNull: true;
943
+ hasDefault: false;
944
+ isPrimaryKey: false;
945
+ isAutoincrement: false;
946
+ hasRuntimeDefault: false;
947
+ enumValues: [string, ...string[]];
948
+ baseColumn: never;
949
+ identity: undefined;
950
+ generated: undefined;
951
+ }, {}, {}>;
952
+ name: import("drizzle-orm/pg-core").PgColumn<{
953
+ name: "name";
954
+ tableName: "user_themes";
955
+ dataType: "string";
956
+ columnType: "PgText";
957
+ data: string;
958
+ driverParam: string;
959
+ notNull: true;
960
+ hasDefault: false;
961
+ isPrimaryKey: false;
962
+ isAutoincrement: false;
963
+ hasRuntimeDefault: false;
964
+ enumValues: [string, ...string[]];
965
+ baseColumn: never;
966
+ identity: undefined;
967
+ generated: undefined;
968
+ }, {}, {}>;
969
+ colors: import("drizzle-orm/pg-core").PgColumn<{
970
+ name: "colors";
971
+ tableName: "user_themes";
972
+ dataType: "json";
973
+ columnType: "PgJsonb";
974
+ data: unknown;
975
+ driverParam: unknown;
976
+ notNull: true;
977
+ hasDefault: true;
978
+ isPrimaryKey: false;
979
+ isAutoincrement: false;
980
+ hasRuntimeDefault: false;
981
+ enumValues: undefined;
982
+ baseColumn: never;
983
+ identity: undefined;
984
+ generated: undefined;
985
+ }, {}, {}>;
986
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
987
+ name: "created_at";
988
+ tableName: "user_themes";
989
+ dataType: "string";
990
+ columnType: "PgTimestampString";
991
+ data: string;
992
+ driverParam: string;
993
+ notNull: true;
994
+ hasDefault: true;
995
+ isPrimaryKey: false;
996
+ isAutoincrement: false;
997
+ hasRuntimeDefault: false;
998
+ enumValues: undefined;
999
+ baseColumn: never;
1000
+ identity: undefined;
1001
+ generated: undefined;
1002
+ }, {}, {}>;
1003
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
1004
+ name: "updated_at";
1005
+ tableName: "user_themes";
1006
+ dataType: "string";
1007
+ columnType: "PgTimestampString";
1008
+ data: string;
1009
+ driverParam: string;
1010
+ notNull: true;
1011
+ hasDefault: true;
1012
+ isPrimaryKey: false;
1013
+ isAutoincrement: false;
1014
+ hasRuntimeDefault: false;
1015
+ enumValues: undefined;
1016
+ baseColumn: never;
1017
+ identity: undefined;
1018
+ generated: undefined;
1019
+ }, {}, {}>;
1020
+ };
1021
+ dialect: "pg";
1022
+ }>;
914
1023
  export declare const catalogues: import("drizzle-orm/pg-core").PgTableWithColumns<{
915
1024
  name: "catalogues";
916
1025
  schema: undefined;
@@ -112,6 +112,21 @@ export const qrConfigs = pgTable("qr_configs", {
112
112
  }).onUpdate("cascade").onDelete("cascade"),
113
113
  unique("qr_configs_catalogue_key").on(table.catalogue),
114
114
  ]);
115
+ export const userThemes = pgTable("user_themes", {
116
+ id: uuid().defaultRandom().primaryKey().notNull(),
117
+ userId: text("user_id").notNull(),
118
+ name: text().notNull(),
119
+ colors: jsonb().default({}).notNull(),
120
+ createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
121
+ updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
122
+ }, (table) => [
123
+ foreignKey({
124
+ columns: [table.userId],
125
+ foreignColumns: [users.id],
126
+ name: "user_themes_user_id_fkey"
127
+ }).onUpdate("cascade").onDelete("cascade"),
128
+ unique("user_themes_user_id_name_key").on(table.userId, table.name),
129
+ ]);
115
130
  export const catalogues = pgTable("catalogues", {
116
131
  id: uuid().defaultRandom().primaryKey().notNull(),
117
132
  name: text().notNull(),
@@ -15,6 +15,10 @@ export type Catalogue = Update<RawCatalogue, {
15
15
  partners: Partner[];
16
16
  metadata: Metadata;
17
17
  }>;
18
+ type RawSavedTheme = InferSelectModel<typeof schema.userThemes>;
19
+ export type SavedTheme = Update<RawSavedTheme, {
20
+ colors: CustomThemeColors;
21
+ }>;
18
22
  export type Metadata = {
19
23
  title: string;
20
24
  description: string;
@@ -87,10 +91,12 @@ export type Legal = {
87
91
  privacyPolicy: string;
88
92
  address: string;
89
93
  };
94
+ export type CustomThemeColors = Partial<Record<"background" | "heading" | "text" | "primary" | "secondary" | "cardBackground", string>>;
90
95
  export type Appearance = {
91
96
  theme: {
92
97
  type: ThemeType;
93
98
  name: string;
99
+ colors?: CustomThemeColors;
94
100
  };
95
101
  style: {
96
102
  contentFontSize: FontSize;
@@ -44,6 +44,7 @@ export type PricingPlan = {
44
44
  apperance: {
45
45
  standardThemes: boolean;
46
46
  styles: boolean;
47
+ customThemes: boolean;
47
48
  };
48
49
  };
49
50
  billing_period?: "month" | "year";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quicktalog/common",
3
- "version": "1.52.0",
3
+ "version": "1.54.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/nikolamirilo/quicktalog-packages.git"
@@ -17,6 +17,9 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "tsc",
20
+ "release": "npm version minor && git push --follow-tags",
21
+ "release:patch": "npm version patch && git push --follow-tags",
22
+ "release:major": "npm version major && git push --follow-tags",
20
23
  "prepare": "husky"
21
24
  },
22
25
  "devDependencies": {