nomkit 0.0.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.
Files changed (82) hide show
  1. package/LICENSE.txt +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +27 -0
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/index.js +6 -0
  5. package/dist/cli/commands/push.d.ts +6 -0
  6. package/dist/cli/commands/push.js +143 -0
  7. package/dist/cli/index.d.ts +4 -0
  8. package/dist/cli/index.js +18 -0
  9. package/dist/cli/lib/collection_sync.d.ts +107 -0
  10. package/dist/cli/lib/collection_sync.js +158 -0
  11. package/dist/cli/lib/config_loader.d.ts +15 -0
  12. package/dist/cli/lib/config_loader.js +43 -0
  13. package/dist/cli/lib/hash.d.ts +22 -0
  14. package/dist/cli/lib/hash.js +63 -0
  15. package/dist/cli/lib/migrations.d.ts +6 -0
  16. package/dist/cli/lib/migrations.js +17 -0
  17. package/dist/client/index.d.ts +13 -0
  18. package/dist/client/index.js +34 -0
  19. package/dist/core/nomba_api/banks.d.ts +14 -0
  20. package/dist/core/nomba_api/banks.js +0 -0
  21. package/dist/core/nomba_api/charge-tokenized-card.d.ts +33 -0
  22. package/dist/core/nomba_api/charge-tokenized-card.js +0 -0
  23. package/dist/core/nomba_api/checkout.d.ts +44 -0
  24. package/dist/core/nomba_api/checkout.js +0 -0
  25. package/dist/core/nomba_api/get_checkout.d.ts +57 -0
  26. package/dist/core/nomba_api/get_checkout.js +0 -0
  27. package/dist/core/nomba_api/index.d.ts +313 -0
  28. package/dist/core/nomba_api/index.js +179 -0
  29. package/dist/core/nomba_api/lib/utils.d.ts +235 -0
  30. package/dist/core/nomba_api/lib/utils.js +313 -0
  31. package/dist/core/nomba_api/list-tokenized-cards.d.ts +24 -0
  32. package/dist/core/nomba_api/list-tokenized-cards.js +0 -0
  33. package/dist/core/nomba_api/token-manager/index.d.ts +51 -0
  34. package/dist/core/nomba_api/token-manager/index.js +109 -0
  35. package/dist/core/pg_db/index.d.ts +108 -0
  36. package/dist/core/pg_db/index.js +76 -0
  37. package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/migration.sql +120 -0
  38. package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/snapshot.json +1616 -0
  39. package/dist/core/pg_db/relations.d.ts +46 -0
  40. package/dist/core/pg_db/relations.js +83 -0
  41. package/dist/core/pg_db/schema.d.ts +1138 -0
  42. package/dist/core/pg_db/schema.js +124 -0
  43. package/dist/endpoints/customers/api.js +51 -0
  44. package/dist/endpoints/entitlements/api.js +42 -0
  45. package/dist/endpoints/routes.d.ts +15 -0
  46. package/dist/endpoints/routes.js +15 -0
  47. package/dist/endpoints/subscriptions/api.js +263 -0
  48. package/dist/endpoints/subscriptions/utils.js +105 -0
  49. package/dist/endpoints/webhooks/invoice/api.js +28 -0
  50. package/dist/endpoints/webhooks/nomba/api.js +76 -0
  51. package/dist/endpoints/webhooks/nomba/utils.js +36 -0
  52. package/dist/index.d.ts +204 -0
  53. package/dist/index.js +175 -0
  54. package/dist/lib/utils.d.ts +21 -0
  55. package/dist/lib/utils.js +41 -0
  56. package/dist/node_modules/.pnpm/@better-fetch_fetch@1.3.1/node_modules/@better-fetch/fetch/dist/index.js +475 -0
  57. package/dist/package.js +4 -0
  58. package/dist/queue/backends/pglite/backend.d.ts +43 -0
  59. package/dist/queue/backends/pglite/backend.js +33 -0
  60. package/dist/queue/backends/pglite/index.d.ts +4 -0
  61. package/dist/queue/backends/pglite/index.js +4 -0
  62. package/dist/queue/backends/pglite/migrations/schema.d.ts +4 -0
  63. package/dist/queue/backends/pglite/migrations/schema.js +37 -0
  64. package/dist/queue/backends/pglite/notification-channel.d.ts +17 -0
  65. package/dist/queue/backends/pglite/notification-channel.js +61 -0
  66. package/dist/queue/backends/pglite/repository.d.ts +38 -0
  67. package/dist/queue/backends/pglite/repository.js +299 -0
  68. package/dist/queue/backends/redis/index.d.ts +7 -0
  69. package/dist/queue/backends/redis/index.js +1 -0
  70. package/dist/queue/client/index.d.ts +12 -0
  71. package/dist/queue/client/index.js +31 -0
  72. package/dist/queue/endpoints/api.d.ts +53 -0
  73. package/dist/queue/endpoints/api.js +45 -0
  74. package/dist/queue/endpoints/routes.d.ts +32 -0
  75. package/dist/queue/endpoints/routes.js +5 -0
  76. package/dist/queue/init.d.ts +27 -0
  77. package/dist/queue/init.js +31 -0
  78. package/dist/queue/lib/billing.d.ts +25 -0
  79. package/dist/queue/lib/billing.js +87 -0
  80. package/dist/queue/lib/utils.d.ts +30 -0
  81. package/dist/queue/lib/utils.js +35 -0
  82. package/package.json +71 -0
package/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Emmanuel Jonah Ajike
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,27 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __exportAll = (all, no_symbols) => {
7
+ let target = {};
8
+ for (var name in all) __defProp(target, name, {
9
+ get: all[name],
10
+ enumerable: true
11
+ });
12
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ return target;
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
17
+ key = keys[i];
18
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
19
+ get: ((k) => from[k]).bind(null, key),
20
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
21
+ });
22
+ }
23
+ return to;
24
+ };
25
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
26
+ //#endregion
27
+ export { __exportAll, __reExport };
@@ -0,0 +1,15 @@
1
+ import { Pool } from "pg";
2
+ import { PGlite } from "@electric-sql/pglite";
3
+
4
+ //#region lib/adapters/index.d.ts
5
+ type AdapterConfig = {
6
+ provider: "pg";
7
+ db: Pool;
8
+ } | {
9
+ provider: "pglite";
10
+ db: PGlite;
11
+ };
12
+ type DatabaseConfig = string | Pool | AdapterConfig;
13
+ declare function drizzleAdapter(args: AdapterConfig): AdapterConfig;
14
+ //#endregion
15
+ export { AdapterConfig, DatabaseConfig, drizzleAdapter };
@@ -0,0 +1,6 @@
1
+ //#region lib/adapters/index.ts
2
+ function drizzleAdapter(args) {
3
+ return args;
4
+ }
5
+ //#endregion
6
+ export { drizzleAdapter };
@@ -0,0 +1,6 @@
1
+ import { Command } from "commander";
2
+
3
+ //#region cli/commands/push.d.ts
4
+ declare const pushCommand: Command;
5
+ //#endregion
6
+ export { pushCommand };
@@ -0,0 +1,143 @@
1
+ import { closeConnection, getDrizzle, hasPendingMigrations } from "../../core/pg_db/index.js";
2
+ import { createCollection, diffCollection, hashCollection, queryLatestCollection } from "../lib/collection_sync.js";
3
+ import { getNomKitConfig } from "../lib/config_loader.js";
4
+ import { dbMigrate } from "../lib/migrations.js";
5
+ import { z as z$1 } from "zod";
6
+ import { access, constants } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { Command } from "commander";
9
+ import ora from "ora";
10
+ //#region cli/commands/push.ts
11
+ const PushOptionsSchema = z$1.object({
12
+ preview: z$1.boolean().optional().default(false),
13
+ cwd: z$1.string(),
14
+ config: z$1.string(),
15
+ removeStale: z$1.boolean().optional().default(false)
16
+ });
17
+ const pushCommand = new Command("push").description("Push the current collection").option("-p, --preview", "Preview without making changes", false).option("--cwd <path>", "Working directory", process.cwd()).option("--config <path>", "Custom Nomkit config path").option("--remove-stale", "Removes collections that are no longer defined in config").action(async (rawOptions) => {
18
+ const startedAt = performance.now();
19
+ const schema = PushOptionsSchema.safeParse(rawOptions);
20
+ if (!schema.success) {
21
+ console.error(z$1.prettifyError(schema.error));
22
+ process.exit(1);
23
+ }
24
+ const options = schema.data;
25
+ const cwd = path.resolve(options.cwd);
26
+ if (options.preview) console.log(" Preview mode — no changes will be written\n");
27
+ const spinner = ora({ color: "cyan" });
28
+ const configPath = await findNomKitConfig(cwd, options.config);
29
+ if (!configPath) {
30
+ spinner.fail("No Nomkit config found. Add a `nomkit.ts` file or pass `--config <path>`.");
31
+ process.exit(1);
32
+ }
33
+ const config = await getNomKitConfig({
34
+ cwd,
35
+ configPath
36
+ });
37
+ try {
38
+ const db = getDrizzle(config.options.database);
39
+ spinner.start("Checking for pending migrations…");
40
+ if (await hasPendingMigrations(db)) {
41
+ spinner.text = "Applying database migrations…";
42
+ await dbMigrate(db);
43
+ spinner.succeed("Database migrations applied");
44
+ } else spinner.succeed("Database is up to date");
45
+ const collection = config.options.products;
46
+ const localCollection = hashCollection(collection);
47
+ const productIds = [];
48
+ const allFeatureIds = [];
49
+ collection.forEach((p) => {
50
+ productIds.push(p.id);
51
+ for (const feature of p.includes ?? []) allFeatureIds.push(feature.id);
52
+ });
53
+ spinner.start("Fetching remote collection…");
54
+ const [dbCollection] = await Promise.all([queryLatestCollection({
55
+ db,
56
+ productIds
57
+ })]);
58
+ spinner.succeed("Remote collection fetched");
59
+ spinner.start(options.preview ? "Previewing changes…" : "Pushing changes…");
60
+ if (!dbCollection || dbCollection.length === 0) {
61
+ const result = await createCollection({
62
+ db,
63
+ localCollection,
64
+ preview: options.preview
65
+ });
66
+ spinner.succeed(options.preview ? "Preview complete (no changes written)" : `Created ${result.actions.length} plan${result.actions.length === 1 ? "" : "s"}`);
67
+ console.log();
68
+ for (const action of result.actions) {
69
+ const features = action.featureCount === 1 ? "1 feature" : action.featureCount === 0 ? "no features" : `${action.featureCount} features`;
70
+ console.log(` ✦ Created ${action.productId} v${action.version} (${features})`);
71
+ }
72
+ } else {
73
+ const result = await diffCollection({
74
+ db,
75
+ localCollection,
76
+ dbCollection,
77
+ preview: options.preview
78
+ });
79
+ const created = result.actions.filter((a) => a.type === "created").length;
80
+ const updated = result.actions.filter((a) => a.type === "updated").length;
81
+ const skipped = result.actions.filter((a) => a.type === "skipped").length;
82
+ const summary = [
83
+ created && `${created} created`,
84
+ updated && `${updated} updated`,
85
+ skipped && `${skipped} skipped`
86
+ ].filter(Boolean).join(" · ");
87
+ spinner.succeed(options.preview ? `Preview complete — ${summary}` : `Push complete — ${summary}`);
88
+ if (result.actions.length > 0) {
89
+ console.log();
90
+ for (const action of result.actions) {
91
+ if (action.type === "created") console.log(` ✦ Created ${action.productId} v${action.version}`);
92
+ if (action.type === "updated") console.log(` ↑ Updated ${action.productId} v${action.previousVersion} → v${action.version}`);
93
+ if (action.type === "skipped") console.log(` — Skipped ${action.productId} (no changes)`);
94
+ }
95
+ }
96
+ }
97
+ const end = performance.now();
98
+ const elapsed = Math.ceil(end - startedAt);
99
+ console.log(`\n Done in ${elapsed}ms\n`);
100
+ } catch (err) {
101
+ spinner.fail(err instanceof Error ? err.message : "Unexpected error");
102
+ console.error(err);
103
+ process.exit(1);
104
+ } finally {
105
+ await closeConnection(config.options.database);
106
+ }
107
+ });
108
+ async function fileExists(path) {
109
+ try {
110
+ await access(path, constants.F_OK);
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+ async function findNomKitConfig(cwd, config = "") {
117
+ const CONFIG_FILENAMES = [
118
+ "nomkit.ts",
119
+ "nomkit.tsx",
120
+ "nomkit.js",
121
+ "nomkit.jsx",
122
+ "nomkit/index.ts",
123
+ "nomkit/index.tsx",
124
+ "nomkit/index.js",
125
+ "nomkit/index.jsx",
126
+ ...[config]
127
+ ];
128
+ const POSSIBLE_CONFIG_PATHS = [
129
+ "",
130
+ "nomkit",
131
+ "lib",
132
+ "src",
133
+ "src/lib",
134
+ "app"
135
+ ].flatMap((dir) => CONFIG_FILENAMES.map((file) => dir === "" ? file : path.join(dir, file)));
136
+ for (const relativePath of POSSIBLE_CONFIG_PATHS) {
137
+ const absolutePath = path.join(cwd, relativePath);
138
+ if (await fileExists(absolutePath)) return absolutePath;
139
+ }
140
+ return null;
141
+ }
142
+ //#endregion
143
+ export { pushCommand };
@@ -0,0 +1,4 @@
1
+ //#region cli/index.d.ts
2
+ declare function getVersion(): string;
3
+ //#endregion
4
+ export { getVersion };
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { pushCommand } from "./commands/push.js";
3
+ import { version } from "../package.js";
4
+ import { Command } from "commander";
5
+ //#region cli/index.ts
6
+ function getVersion() {
7
+ return version;
8
+ }
9
+ const program = new Command();
10
+ program.name("nomkit").description("Nomkit is an embedded subscription billing framework that lets developers define products, pricing, and feature entitlements in code, while using payment providers like Nomba for payment processing and automatically syncing billing data to the database.").version(getVersion(), "-v, --version", "display version number");
11
+ program.addCommand(pushCommand);
12
+ try {
13
+ program.parse();
14
+ } catch (error) {
15
+ console.error(error);
16
+ }
17
+ //#endregion
18
+ export { getVersion };
@@ -0,0 +1,107 @@
1
+ import { NomkitDB } from "../../core/pg_db/index.js";
2
+ import { FeatureOpts, MeteredOpts, NomKitOptions, Plan } from "../../index.js";
3
+
4
+ //#region cli/lib/collection_sync.d.ts
5
+ type Feature = FeatureOpts & MeteredOpts;
6
+ type Hashed<T> = T & {
7
+ hash: string;
8
+ };
9
+ type LocalFeature = Hashed<Feature> & {
10
+ _planId: string;
11
+ };
12
+ type LocalProduct = Hashed<Omit<Plan, "includes">> & {
13
+ features: LocalFeature[];
14
+ };
15
+ type HashedLocalCollection = LocalProduct;
16
+ declare function hashCollection(products: NomKitOptions["products"]): LocalProduct[];
17
+ type DbCollection = {
18
+ internalId: string;
19
+ id: string;
20
+ label: string;
21
+ hash: string;
22
+ priceAmount: number | null;
23
+ priceInterval: string | null;
24
+ version: number;
25
+ createdAt: Date;
26
+ updatedAt: Date;
27
+ features: {
28
+ internalId: string;
29
+ hash: string;
30
+ createdAt: Date;
31
+ updatedAt: Date;
32
+ featureInternalId: string;
33
+ type: "boolean" | "metered";
34
+ limit: bigint | null;
35
+ resetInterval: string | null;
36
+ productId: string;
37
+ productVersion: number;
38
+ feature: {
39
+ internalId: string;
40
+ id: string;
41
+ label: string;
42
+ createdAt: Date;
43
+ updatedAt: Date;
44
+ } | null;
45
+ }[];
46
+ };
47
+ declare function queryLatestCollection({
48
+ db,
49
+ productIds
50
+ }: {
51
+ db: NomkitDB;
52
+ productIds: string[];
53
+ }): Promise<DbCollection[]>;
54
+ interface CreateCollection {
55
+ db: NomkitDB;
56
+ localCollection: HashedLocalCollection[];
57
+ preview: boolean;
58
+ }
59
+ interface CreateResult {
60
+ actions: CreateAction[];
61
+ created: number;
62
+ }
63
+ type CreateAction = {
64
+ type: "created";
65
+ productId: string;
66
+ version: number;
67
+ featureCount: number;
68
+ };
69
+ declare function createCollection({
70
+ db,
71
+ localCollection,
72
+ preview
73
+ }: CreateCollection): Promise<CreateResult>;
74
+ type DiffAction = {
75
+ type: "created";
76
+ productId: string;
77
+ version: number;
78
+ } | {
79
+ type: "updated";
80
+ productId: string;
81
+ version: number;
82
+ previousVersion: number;
83
+ } | {
84
+ type: "skipped";
85
+ productId: string;
86
+ reason: "hash_match";
87
+ };
88
+ interface DiffResult {
89
+ actions: DiffAction[];
90
+ created: number;
91
+ updated: number;
92
+ skipped: number;
93
+ }
94
+ interface DiffOptions {
95
+ db: NomkitDB;
96
+ localCollection: HashedLocalCollection[];
97
+ dbCollection: DbCollection[];
98
+ preview: boolean;
99
+ }
100
+ declare function diffCollection({
101
+ db,
102
+ localCollection,
103
+ dbCollection,
104
+ preview
105
+ }: DiffOptions): Promise<DiffResult>;
106
+ //#endregion
107
+ export { CreateAction, CreateResult, DiffAction, DiffResult, Feature, createCollection, diffCollection, hashCollection, queryLatestCollection };
@@ -0,0 +1,158 @@
1
+ import { features, productFeatures, products } from "../../core/pg_db/schema.js";
2
+ import { hashFeature, hashPlan } from "./hash.js";
3
+ import { sql } from "drizzle-orm";
4
+ //#region cli/lib/collection_sync.ts
5
+ const INITIAL_PRODUCT_VERSION = 1;
6
+ function hashCollection(products) {
7
+ return products.map((product) => {
8
+ const productHash = hashPlan(product);
9
+ const { includes = [], ...rest } = product;
10
+ return {
11
+ id: rest.id,
12
+ label: rest.label,
13
+ hash: productHash,
14
+ price: rest.price,
15
+ features: includes.map((feature) => {
16
+ return {
17
+ id: feature.id,
18
+ label: feature.label,
19
+ limit: feature.limit,
20
+ reset: feature.reset,
21
+ type: feature.type,
22
+ hash: hashFeature(feature),
23
+ _planId: product.id
24
+ };
25
+ })
26
+ };
27
+ });
28
+ }
29
+ async function queryLatestCollection({ db, productIds }) {
30
+ return (await Promise.all(productIds.map((id) => {
31
+ return db.query.products.findFirst({
32
+ where: { id },
33
+ orderBy: (products, { desc }) => [desc(products.version)],
34
+ with: { features: { with: { feature: true } } }
35
+ });
36
+ }))).filter((r) => r !== void 0);
37
+ }
38
+ async function createCollection({ db, localCollection, preview = false }) {
39
+ const actions = localCollection.map((p) => ({
40
+ type: "created",
41
+ productId: p.id,
42
+ version: INITIAL_PRODUCT_VERSION,
43
+ featureCount: p.features.length
44
+ }));
45
+ if (!preview) {
46
+ const newProducts = localCollection.map((p) => buildProduct(p, INITIAL_PRODUCT_VERSION));
47
+ const newFeatures = localCollection.flatMap((p) => p.features).map((f) => ({
48
+ id: f.id,
49
+ label: f.label
50
+ }));
51
+ await db.transaction(async (tx) => {
52
+ const products$1 = await tx.insert(products).values(newProducts).returning();
53
+ const features$2 = await tx.insert(features).values(newFeatures).onConflictDoUpdate({
54
+ target: features.id,
55
+ set: {
56
+ internalId: sql`excluded.internal_id`,
57
+ id: sql`excluded.id`,
58
+ label: sql`excluded.label`
59
+ }
60
+ }).returning();
61
+ const newProductFeatures = localCollection.flatMap((p) => p.features).map((configFeature) => {
62
+ const product = products$1.find((p) => p.id === configFeature._planId);
63
+ if (!product) throw new Error(`Could not find product for feature: ${configFeature.id}`);
64
+ const feature = features$2.find((f) => f.id === configFeature.id && f.label === configFeature.label);
65
+ if (!feature) throw new Error(`Could not find feature: ${configFeature.id}`);
66
+ return buildProductFeature(configFeature, feature.internalId, product.id, product.version);
67
+ });
68
+ await tx.insert(productFeatures).values(newProductFeatures);
69
+ });
70
+ }
71
+ return {
72
+ actions,
73
+ created: actions.length
74
+ };
75
+ }
76
+ async function diffCollection({ db, localCollection, dbCollection, preview = false }) {
77
+ const actions = [];
78
+ for (const configProduct of localCollection) {
79
+ const dbProduct = dbCollection.find((p) => p.id === configProduct.id);
80
+ if (!dbProduct) {
81
+ const version = INITIAL_PRODUCT_VERSION;
82
+ actions.push({
83
+ type: "created",
84
+ productId: configProduct.id,
85
+ version
86
+ });
87
+ if (!preview) await db.transaction((tx) => upsertProductWithFeatures(tx, configProduct, version));
88
+ } else if (dbProduct.hash !== configProduct.hash) {
89
+ const previousVersion = dbProduct.version;
90
+ const version = previousVersion + 1;
91
+ actions.push({
92
+ type: "updated",
93
+ productId: configProduct.id,
94
+ version,
95
+ previousVersion
96
+ });
97
+ if (!preview) await db.transaction((tx) => upsertProductWithFeatures(tx, configProduct, version));
98
+ } else actions.push({
99
+ type: "skipped",
100
+ productId: configProduct.id,
101
+ reason: "hash_match"
102
+ });
103
+ }
104
+ return {
105
+ actions,
106
+ created: actions.filter((a) => a.type === "created").length,
107
+ updated: actions.filter((a) => a.type === "updated").length,
108
+ skipped: actions.filter((a) => a.type === "skipped").length
109
+ };
110
+ }
111
+ function buildProduct(configProduct, version) {
112
+ const product = {
113
+ id: configProduct.id,
114
+ label: configProduct.label,
115
+ hash: configProduct.hash,
116
+ version
117
+ };
118
+ if (configProduct.price) {
119
+ product.priceAmount = configProduct.price.amount;
120
+ product.priceInterval = configProduct.price.interval;
121
+ }
122
+ return product;
123
+ }
124
+ function buildProductFeature(configFeature, featureInternalId, productId, productVersion) {
125
+ const feature = {
126
+ type: configFeature.type,
127
+ hash: configFeature.hash,
128
+ productId,
129
+ productVersion,
130
+ featureInternalId
131
+ };
132
+ if (configFeature.type === "metered") {
133
+ feature.limit = BigInt(configFeature.limit);
134
+ feature.resetInterval = configFeature.reset;
135
+ }
136
+ return feature;
137
+ }
138
+ async function upsertProductWithFeatures(tx, configProduct, version) {
139
+ await tx.insert(products).values(buildProduct(configProduct, version));
140
+ if (configProduct.features.length === 0) return;
141
+ const features$1 = await tx.insert(features).values(configProduct.features.map((f) => ({
142
+ id: f.id,
143
+ label: f.label
144
+ }))).onConflictDoUpdate({
145
+ target: features.id,
146
+ set: {
147
+ id: sql`excluded.id`,
148
+ label: sql`excluded.label`
149
+ }
150
+ }).returning();
151
+ await tx.insert(productFeatures).values(configProduct.features.map((configFeature) => {
152
+ const feature = features$1.find((f) => f.id === configFeature.id);
153
+ if (!feature) throw new Error(`Could not find feature: ${configFeature.id}`);
154
+ return buildProductFeature(configFeature, feature.internalId, configProduct.id, version);
155
+ }));
156
+ }
157
+ //#endregion
158
+ export { createCollection, diffCollection, hashCollection, queryLatestCollection };
@@ -0,0 +1,15 @@
1
+ import { NomKitOptions } from "../../index.js";
2
+
3
+ //#region cli/lib/config_loader.d.ts
4
+ declare function getNomKitConfig({
5
+ cwd,
6
+ configPath
7
+ }: {
8
+ cwd: string;
9
+ configPath?: string;
10
+ }): Promise<{
11
+ path: string;
12
+ options: NomKitOptions;
13
+ }>;
14
+ //#endregion
15
+ export { getNomKitConfig };
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import dotenv from "dotenv";
3
+ import { createJiti } from "jiti";
4
+ //#region cli/lib/config_loader.ts
5
+ function loadDotEnv(cwd) {
6
+ dotenv.config({
7
+ path: path.join(cwd, ".env"),
8
+ quiet: true
9
+ });
10
+ dotenv.config({
11
+ override: true,
12
+ path: path.join(cwd, ".env.local"),
13
+ quiet: true
14
+ });
15
+ }
16
+ async function loadModule(cwd, configPath) {
17
+ loadDotEnv(cwd);
18
+ return createJiti(configPath, {
19
+ interopDefault: false,
20
+ jsx: true,
21
+ moduleCache: false
22
+ }).import(configPath);
23
+ }
24
+ function getNomKit(module) {
25
+ if (!module || typeof module !== "object") return null;
26
+ const exports = module;
27
+ return [exports.nomkit, exports.default].find((value) => isNomKitLike(value)) ?? null;
28
+ }
29
+ function isNomKitLike(value) {
30
+ if (!value || typeof value !== "object") return false;
31
+ const nomkit = value;
32
+ return typeof nomkit?.handler === "function" && typeof nomkit?.api?.subscribe === "function" && "options" in nomkit;
33
+ }
34
+ async function getNomKitConfig({ cwd, configPath = "" }) {
35
+ const config = getNomKit(await loadModule(cwd, configPath));
36
+ if (!config) throw new Error(`Couldn't read your NomKit instance in ${configPath}. Export your NomKit instance as \`nomkit\` or default export the result of \`createNomkit(...)\`.`);
37
+ return {
38
+ path: configPath,
39
+ options: config.options
40
+ };
41
+ }
42
+ //#endregion
43
+ export { getNomKitConfig };
@@ -0,0 +1,22 @@
1
+ import { Plan } from "../../index.js";
2
+ import { Feature } from "./collection_sync.js";
3
+
4
+ //#region cli/lib/hash.d.ts
5
+ declare function stabilizeFeature(feature: Feature): {
6
+ id: string;
7
+ type: "boolean" | "metered";
8
+ limit: number;
9
+ reset: "week" | "month" | "year";
10
+ };
11
+ declare function stabilizePlan(plan: Plan<any>): {
12
+ id: string;
13
+ price?: {
14
+ amount: number;
15
+ interval: string;
16
+ };
17
+ features: Record<string, unknown>;
18
+ };
19
+ declare const hashFeature: (feature: Feature) => string;
20
+ declare const hashPlan: (plan: Plan) => string;
21
+ //#endregion
22
+ export { hashFeature, hashPlan, stabilizeFeature, stabilizePlan };
@@ -0,0 +1,63 @@
1
+ import z from "zod";
2
+ import crypto from "node:crypto";
3
+ //#region cli/lib/hash.ts
4
+ function sortedStringify(obj) {
5
+ return JSON.stringify(obj, (_, value) => {
6
+ if (value && typeof value === "object" && !Array.isArray(value)) return Object.keys(value).sort().reduce((sorted, k) => {
7
+ sorted[k] = value[k];
8
+ return sorted;
9
+ }, {});
10
+ return value;
11
+ }, 0);
12
+ }
13
+ function hashString(data) {
14
+ return crypto.createHash("sha256").update(data).digest("hex");
15
+ }
16
+ function stabilizeFeature(feature) {
17
+ const { label, ...stableProperties } = feature;
18
+ return stableProperties;
19
+ }
20
+ function stabilizePlan(plan) {
21
+ const idAndLabel = {
22
+ id: z.string().min(1),
23
+ label: z.string().min(1)
24
+ };
25
+ const result = z.object({
26
+ ...idAndLabel,
27
+ price: z.object({
28
+ amount: z.number().min(0),
29
+ interval: z.string().min(1)
30
+ }).optional(),
31
+ includes: z.array(z.discriminatedUnion("type", [z.object({
32
+ ...idAndLabel,
33
+ type: z.literal("boolean")
34
+ }), z.object({
35
+ ...idAndLabel,
36
+ type: z.literal("metered"),
37
+ limit: z.number(),
38
+ reset: z.string().min(1)
39
+ })])).optional()
40
+ }).strict().safeParse(plan);
41
+ if (!result.success) {
42
+ console.error(z.prettifyError(result.error));
43
+ throw new Error("Plan validation failed");
44
+ }
45
+ const features = {};
46
+ for (const feature of plan?.includes ?? []) {
47
+ const { id, ...stableProperties } = stabilizeFeature(feature);
48
+ features[feature.id] = stableProperties;
49
+ }
50
+ const { label, includes, ...stableProperties } = plan;
51
+ return {
52
+ features,
53
+ ...stableProperties
54
+ };
55
+ }
56
+ const hashFeature = (feature) => {
57
+ return hashString(sortedStringify(stabilizeFeature(feature)));
58
+ };
59
+ const hashPlan = (plan) => {
60
+ return hashString(sortedStringify(stabilizePlan(plan)));
61
+ };
62
+ //#endregion
63
+ export { hashFeature, hashPlan, stabilizeFeature, stabilizePlan };
@@ -0,0 +1,6 @@
1
+ import { NomkitDB } from "../../core/pg_db/index.js";
2
+
3
+ //#region cli/lib/migrations.d.ts
4
+ declare function dbMigrate(db: NomkitDB): Promise<void>;
5
+ //#endregion
6
+ export { dbMigrate };
@@ -0,0 +1,17 @@
1
+ import { isPGlite, isPgPool, migratePGlite, migratePgNode } from "../../core/pg_db/index.js";
2
+ //#region cli/lib/migrations.ts
3
+ async function dbMigrate(db) {
4
+ const client = db.$client;
5
+ if (isPGlite(client)) {
6
+ try {
7
+ await migratePGlite(client);
8
+ } finally {}
9
+ return;
10
+ }
11
+ if (!isPgPool(client)) return;
12
+ try {
13
+ await migratePgNode(client);
14
+ } finally {}
15
+ }
16
+ //#endregion
17
+ export { dbMigrate };