Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: install

on:
push:
branches: [development]
pull_request:
branches: [development]
workflow_dispatch:

jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: 24
- name: Configure npm global prefix
run: |
npm config set prefix "$HOME/.npm-global"
echo "$HOME/.npm-global/bin" >> "$GITHUB_PATH"
- run: npm install
- name: Pack CLI
id: pack
run: |
TARBALL=$(npm pack | tail -1 | tr -d '\r')
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
- name: Install packed CLI globally
run: npm install -g "./${{ steps.pack.outputs.tarball }}"
- name: Exercise CLI
run: bp-run --help
- name: Cleanup global install
run: npm uninstall -g @theprofs/bp-run
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ jobs:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: 22
node-version: 24
- run: npm install
- run: npm test
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ for await (const user of users()) {

for await (const user of users()) {
await user.set({
stripe_subsription_status: user.subscription?.status || null
stripe_subscription_status: user.subscription?.status || null
}).save()
}
````
Expand Down
16 changes: 8 additions & 8 deletions dist/cli.js
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class User {
if (!changed.length)
return this;
log.info('Saving user $1 ($2)', [this.id, changed.join(', ')]);
const ident = (s) => `"${String(s).replace(/\"/g, '""')}"`;
const ident = (s) => `"${String(s).replace(/"/g, '""')}"`;
const sets = changed.map((key, i) => `${ident(key)} = $${i + 1}`).join(', ');
const values = changed.map(key => this[key]);
await this._db.query(`UPDATE users SET ${sets} WHERE id = $${changed.length + 1}`, [...values, this.id]);
Expand Down Expand Up @@ -78,7 +78,7 @@ const setKeychain = async (key, value, run = execFileDefault) => {
if (!/^[A-Z_]+$/.test(key))
throw new TypeError('Key must be uppercase letters and underscores only');
try {
await run('/usr/bin/security', ['add-generic-password', '-s', 'BP_CLI', '-a', key, '-w', value]);
await run('/usr/bin/security', ['add-generic-password', '-U', '-s', 'BP_CLI', '-a', key, '-w', value]);
}
catch (err) {
const stderr = err?.stderr?.toString() ?? '';
Expand Down Expand Up @@ -184,14 +184,14 @@ const discoverStripeResources = (stripe) => {
};
const help = () => {
console.error(`${color('bold', 'Usage:')}
bp-sync Show this help
bp-sync -h, --help Show this help
bp-sync init Create mapping.js
bp-sync exec <file> Execute script
bp-run Show this help
bp-run -h, --help Show this help
bp-run init Create mapping.js
bp-run exec <file> Execute script

${color('bold', 'Examples:')}
bp-sync init
bp-sync exec mapping.js`);
bp-run init
bp-run exec mapping.js`);
process.exit(0);
};
const init = async () => {
Expand Down
11 changes: 4 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dist/cli.js"
],
"engines": {
"node": ">=23.6"
"node": ">=24"
},
"scripts": {
"build": "tsc src/cli.ts --outDir dist --module nodenext --target esnext --moduleResolution nodenext --esModuleInterop --skipLibCheck --noCheck",
Expand All @@ -34,9 +34,7 @@
"license": "MIT",
"dependencies": {
"pg": "^8.13.1",
"stripe": "^17.4.0"
},
"devDependencies": {
"stripe": "^17.4.0",
"typescript": "^5.9.3"
}
}
16 changes: 8 additions & 8 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class User {

log.info('Saving user $1 ($2)', [this.id, changed.join(', ')])

const ident = (s: string) => `"${String(s).replace(/\"/g, '""')}"`
const ident = (s: string) => `"${String(s).replace(/"/g, '""')}"`
const sets = changed.map((key, i) => `${ident(key)} = $${i + 1}`).join(', ')
const values = changed.map(key => this[key])

Expand Down Expand Up @@ -136,7 +136,7 @@ const setKeychain = async (
try {
await run(
'/usr/bin/security',
['add-generic-password', '-s', 'BP_CLI', '-a', key, '-w', value]
['add-generic-password', '-U', '-s', 'BP_CLI', '-a', key, '-w', value]
)
} catch (err: unknown) {
const stderr = (err as any)?.stderr?.toString() ?? ''
Expand Down Expand Up @@ -283,14 +283,14 @@ const discoverStripeResources = (stripe: Stripe): Record<string, Function> => {

const help = (): void => {
console.error(`${color('bold', 'Usage:')}
bp-sync Show this help
bp-sync -h, --help Show this help
bp-sync init Create mapping.js
bp-sync exec <file> Execute script
bp-run Show this help
bp-run -h, --help Show this help
bp-run init Create mapping.js
bp-run exec <file> Execute script

${color('bold', 'Examples:')}
bp-sync init
bp-sync exec mapping.js`)
bp-run init
bp-run exec mapping.js`)
process.exit(0)
}

Expand Down
26 changes: 25 additions & 1 deletion test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
createUsersGenerator,
createQueryHelper
} from '../src/cli.ts'
import { mocks } from './utils/index.ts'

import type { Pool as PgPool } from 'pg'
import type Stripe from 'stripe'
Expand Down Expand Up @@ -419,7 +420,7 @@ test('#users', async t => {
} as unknown as PgPool
const stripe = {
customers: {
retrieve: t.mock.fn(async (id: string) => ({ id, email: `${id}@stripe.com` }))
retrieve: t.mock.fn(mocks.stripe.customers.retrieve)
}
} as unknown as Stripe
const users = createUsersGenerator(db, stripe)
Expand All @@ -430,6 +431,29 @@ test('#users', async t => {

assert.ok(items[0].customer)
assert.strictEqual(items[0].customer.id, 'cus_1')
assert.strictEqual(items[0].customer.email, 'cus_1@example.com')
})

await t.test('returns full customer data with mock utilities', async t => {
const db = {
query: t.mock.fn(async () => ({
rows: [{ id: 1, email: 'a@test.com', stripe_id: 'cus_test' }]
}))
} as unknown as PgPool

t.mock.method(mocks.stripe.customers, 'retrieve')

const users = createUsersGenerator(db, mocks.stripe as unknown as Stripe)

const items = []
for await (const user of users('', [], { fetchCustomer: true }))
items.push(user)

assert.ok(items[0].customer)
assert.strictEqual(items[0].customer.id, 'cus_test')
assert.strictEqual(items[0].customer.name, 'Test Customer')
assert.strictEqual(items[0].customer.currency, 'usd')
assert.strictEqual(mocks.stripe.customers.retrieve.mock.callCount(), 1)
})

await t.test('handles missing stripe_id', async t => {
Expand Down
5 changes: 5 additions & 0 deletions test/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as stripeUtils from './stripe.ts'

export const mocks = {
stripe: stripeUtils.stripe
}
62 changes: 62 additions & 0 deletions test/utils/stripe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
export const stripe = {
customers: {
retrieve: async (id: string) => ({
id,
email: `${id}@example.com`,
name: 'Test Customer',
created: Math.floor(Date.now() / 1000),
currency: 'usd',
default_source: null,
delinquent: false,
description: null,
discount: null,
invoice_prefix: null,
invoice_settings: { default_payment_method: null },
livemode: false,
metadata: {},
shipping: null,
tax_exempt: 'none'
}),

list: async (params?: any) => ({
object: 'list',
data: [],
has_more: false,
url: '/v1/customers'
})
},

subscriptions: {
list: async (params?: any) => ({
object: 'list',
data: [],
has_more: false,
url: '/v1/subscriptions'
}),

cancel: async (id: string) => ({
id,
object: 'subscription',
status: 'canceled',
canceled_at: Math.floor(Date.now() / 1000)
})
},

invoices: {
list: async (params?: any) => ({
object: 'list',
data: [],
has_more: false,
url: '/v1/invoices'
})
},

charges: {
list: async (params?: any) => ({
object: 'list',
data: [],
has_more: false,
url: '/v1/charges'
})
}
}