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.
- package/LICENSE.txt +21 -0
- package/dist/_virtual/_rolldown/runtime.js +27 -0
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/index.js +6 -0
- package/dist/cli/commands/push.d.ts +6 -0
- package/dist/cli/commands/push.js +143 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +18 -0
- package/dist/cli/lib/collection_sync.d.ts +107 -0
- package/dist/cli/lib/collection_sync.js +158 -0
- package/dist/cli/lib/config_loader.d.ts +15 -0
- package/dist/cli/lib/config_loader.js +43 -0
- package/dist/cli/lib/hash.d.ts +22 -0
- package/dist/cli/lib/hash.js +63 -0
- package/dist/cli/lib/migrations.d.ts +6 -0
- package/dist/cli/lib/migrations.js +17 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.js +34 -0
- package/dist/core/nomba_api/banks.d.ts +14 -0
- package/dist/core/nomba_api/banks.js +0 -0
- package/dist/core/nomba_api/charge-tokenized-card.d.ts +33 -0
- package/dist/core/nomba_api/charge-tokenized-card.js +0 -0
- package/dist/core/nomba_api/checkout.d.ts +44 -0
- package/dist/core/nomba_api/checkout.js +0 -0
- package/dist/core/nomba_api/get_checkout.d.ts +57 -0
- package/dist/core/nomba_api/get_checkout.js +0 -0
- package/dist/core/nomba_api/index.d.ts +313 -0
- package/dist/core/nomba_api/index.js +179 -0
- package/dist/core/nomba_api/lib/utils.d.ts +235 -0
- package/dist/core/nomba_api/lib/utils.js +313 -0
- package/dist/core/nomba_api/list-tokenized-cards.d.ts +24 -0
- package/dist/core/nomba_api/list-tokenized-cards.js +0 -0
- package/dist/core/nomba_api/token-manager/index.d.ts +51 -0
- package/dist/core/nomba_api/token-manager/index.js +109 -0
- package/dist/core/pg_db/index.d.ts +108 -0
- package/dist/core/pg_db/index.js +76 -0
- package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/migration.sql +120 -0
- package/dist/core/pg_db/migrations/20260703085901_wealthy_blacklash/snapshot.json +1616 -0
- package/dist/core/pg_db/relations.d.ts +46 -0
- package/dist/core/pg_db/relations.js +83 -0
- package/dist/core/pg_db/schema.d.ts +1138 -0
- package/dist/core/pg_db/schema.js +124 -0
- package/dist/endpoints/customers/api.js +51 -0
- package/dist/endpoints/entitlements/api.js +42 -0
- package/dist/endpoints/routes.d.ts +15 -0
- package/dist/endpoints/routes.js +15 -0
- package/dist/endpoints/subscriptions/api.js +263 -0
- package/dist/endpoints/subscriptions/utils.js +105 -0
- package/dist/endpoints/webhooks/invoice/api.js +28 -0
- package/dist/endpoints/webhooks/nomba/api.js +76 -0
- package/dist/endpoints/webhooks/nomba/utils.js +36 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +175 -0
- package/dist/lib/utils.d.ts +21 -0
- package/dist/lib/utils.js +41 -0
- package/dist/node_modules/.pnpm/@better-fetch_fetch@1.3.1/node_modules/@better-fetch/fetch/dist/index.js +475 -0
- package/dist/package.js +4 -0
- package/dist/queue/backends/pglite/backend.d.ts +43 -0
- package/dist/queue/backends/pglite/backend.js +33 -0
- package/dist/queue/backends/pglite/index.d.ts +4 -0
- package/dist/queue/backends/pglite/index.js +4 -0
- package/dist/queue/backends/pglite/migrations/schema.d.ts +4 -0
- package/dist/queue/backends/pglite/migrations/schema.js +37 -0
- package/dist/queue/backends/pglite/notification-channel.d.ts +17 -0
- package/dist/queue/backends/pglite/notification-channel.js +61 -0
- package/dist/queue/backends/pglite/repository.d.ts +38 -0
- package/dist/queue/backends/pglite/repository.js +299 -0
- package/dist/queue/backends/redis/index.d.ts +7 -0
- package/dist/queue/backends/redis/index.js +1 -0
- package/dist/queue/client/index.d.ts +12 -0
- package/dist/queue/client/index.js +31 -0
- package/dist/queue/endpoints/api.d.ts +53 -0
- package/dist/queue/endpoints/api.js +45 -0
- package/dist/queue/endpoints/routes.d.ts +32 -0
- package/dist/queue/endpoints/routes.js +5 -0
- package/dist/queue/init.d.ts +27 -0
- package/dist/queue/init.js +31 -0
- package/dist/queue/lib/billing.d.ts +25 -0
- package/dist/queue/lib/billing.js +87 -0
- package/dist/queue/lib/utils.d.ts +30 -0
- package/dist/queue/lib/utils.js +35 -0
- 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,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,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,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 };
|