Skip to content

Commit

Permalink
Merge branch 'main' into bug/fix-docker
Browse files Browse the repository at this point in the history
  • Loading branch information
paustint committed Feb 20, 2025
2 parents 9ed18a5 + f959a12 commit bb88cdb
Show file tree
Hide file tree
Showing 428 changed files with 11,210 additions and 2,462 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
ENVIRONMENT='development'

# SFDC API VERSION TO USE
NX_SFDC_API_VERSION='62.0'
NX_SFDC_API_VERSION='63.0'

# trace, debug (default), info, warn, error, fatal, silent
LOG_LEVEL='trace'

# If true, will print out logs in a more human readable format instead of JSON (only in dev mode)
PRETTY_LOGS='true'

# Default value for email two-factor authentication for new users
JETSTREAM_AUTH_2FA_EMAIL_DEFAULT_VALUE='false'
# Session signing secret - minimum of 32 characters
Expand Down
65 changes: 52 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
pull_request:

env:
NODE_OPTIONS: '--max_old_space_size=4096'
LOG_LEVEL: warn
CONTENTFUL_HOST: cdn.contentful.com
CONTENTFUL_SPACE: wuv9tl5d77ll
Expand All @@ -21,6 +22,7 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
timeout-minutes: 60

steps:
- uses: actions/checkout@v4
name: Checkout [main]
Expand All @@ -42,14 +44,10 @@ jobs:
- name: install dependencies
run: yarn install --frozen-lockfile

- name: Test all affected projects
env:
NODE_OPTIONS: '--max_old_space_size=4096'
- name: Test all projects
run: yarn test:all

- name: Build
env:
NODE_OPTIONS: '--max_old_space_size=4096'
run: yarn build:ci

- name: Uploading artifacts
Expand Down Expand Up @@ -106,9 +104,53 @@ jobs:
if: always()
run: docker compose down -v

# e2e tests only runs if build passes, since it uses production build to run tests
test-cron:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
PRISMA_TEST_DB_URI: postgres://postgres:postgres@localhost:5432/postgres
JETSTREAM_POSTGRES_DBURI: postgres://postgres:postgres@localhost:5432/postgres

services:
postgres:
image: postgres
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432

steps:
- uses: actions/checkout@v4
name: Checkout [master]
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'

- name: install dependencies
run: yarn install --frozen-lockfile

# Run database migrations
- name: Generate database
run: yarn db:generate

- name: Run database migration
run: yarn db:migrate

- name: Test cron-tasks
run: yarn test:cron

e2e:
needs: build-and-test
runs-on: ubuntu-latest
env:
NX_CLOUD_DISTRIBUTED_EXECUTION: false
Expand All @@ -130,7 +172,7 @@ jobs:
JETSTREAM_SERVER_DOMAIN: localhost:3333
JETSTREAM_SERVER_URL: http://localhost:3333
JETSTREAM_SESSION_SECRET: ${{ secrets.JETSTREAM_SESSION_SECRET }}
SFDC_API_VERSION: '62.0'
SFDC_API_VERSION: '63.0'
SFDC_CALLBACK_URL: http://localhost:3333/oauth/sfdc/callback
SFDC_CONSUMER_KEY: ${{ secrets.SFDC_CONSUMER_KEY }}
SFDC_CONSUMER_SECRET: ${{ secrets.SFDC_CONSUMER_SECRET }}
Expand Down Expand Up @@ -164,11 +206,8 @@ jobs:
- name: install dependencies
run: yarn install --frozen-lockfile

- name: Download artifacts from build
uses: actions/download-artifact@v4
with:
name: dist-artifacts
path: dist
- name: Build
run: yarn build:ci

- name: Install Playwright dependencies
run: npx playwright install --with-deps
Expand Down
6 changes: 3 additions & 3 deletions .release-it-web-ext.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@
"npm": false,
"github": {
"release": true,
"releaseName": "Jetstream Web Extension ${version}",
"assets": []
"releaseName": "Jetstream Web Extension ${version}"
},
"hooks": {
"after:bump": [
"npx prettier --write apps/jetstream-web-extension/src/manifest.json",
"yarn build:web-extension",
"version=v${version} yarn build:web-extension:zip"
"version=v${version} yarn build:web-extension:zip",
"version=v${version} yarn build:web-extension:upload --accept-all"
],
"after:release": "echo Successfully released ${name} v${version}."
}
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile.e2e
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ COPY ./prisma ./prisma/
RUN yarn

# Install other dependencies that were not calculated by nx, but are required
RUN yarn add dotenv prisma@^3.13.0
# Install matching version of prisma by extracting @prisma/client version from package.json,
# and stripping the caret ("^") if present.
RUN yarn add prisma@$(node -p "require('./package.json')['dependencies']['@prisma/client'].replace('^','')")

# Generate prisma client - ensure that there are no OS differences
RUN npx prisma generate
Expand Down
4 changes: 2 additions & 2 deletions apps/api/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ JETSTREAM_SERVER_DOMAIN="getjetstream.app"
JETSTREAM_SERVER_URL="https://getjetstream.app"

NX_BRANCH="main"
NX_SFDC_API_VERSION="62.0"
NX_SFDC_API_VERSION="63.0"

SFDC_API_VERSION="62.0"
SFDC_API_VERSION="63.0"
SFDC_CALLBACK_URL="https://getjetstream.app/oauth/sfdc/callback"
56 changes: 32 additions & 24 deletions apps/api/src/app/controllers/billing.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ENV } from '@jetstream/api-config';
import { ENV, logger } from '@jetstream/api-config';
import { UserProfileUi } from '@jetstream/types';
import { Request, Response } from 'express';
import { z } from 'zod';
import * as userDbService from '../db/user.db';
Expand Down Expand Up @@ -86,38 +87,45 @@ const createCheckoutSessionHandler = createRoute(
}
);

const processCheckoutSuccessHandler = createRoute(
routeDefinition.processCheckoutSuccess.validators,
async ({ user: sessionUser, query }, req, res) => {
ensureStripeIsInitialized();
const { subscribeAction, sessionId } = query;

if (!subscribeAction || !sessionId) {
return redirect(res, `${ENV.JETSTREAM_CLIENT_URL}/settings/billing`);
}
const processCheckoutSuccessHandler = createRoute(routeDefinition.processCheckoutSuccess.validators, async ({ user, query }, req, res) => {
ensureStripeIsInitialized();
const { subscribeAction, sessionId } = query;

const user = await userDbService.findByIdWithSubscriptions(sessionUser.id);
if (!user.subscriptions?.[0]?.customerId) {
return redirect(res, `${ENV.JETSTREAM_CLIENT_URL}/settings/billing`);
}
if (!subscribeAction || !sessionId) {
return redirect(res, `${ENV.JETSTREAM_CLIENT_URL}/settings/billing`);
}

await stripeService.saveSubscriptionFromCompletedSession({ sessionId });
await stripeService.saveSubscriptionFromCompletedSession({ sessionId });

redirect(res, `${ENV.JETSTREAM_CLIENT_URL}/settings/billing`);
}
);
redirect(res, `${ENV.JETSTREAM_CLIENT_URL}/settings/billing`);
});

const getSubscriptionsHandler = createRoute(routeDefinition.getSubscriptions.validators, async ({ user }, req, res) => {
ensureStripeIsInitialized();
const userProfile = await userDbService.findById(user.id);
// No billing account, so there are no subscriptions
if (!userProfile.billingAccount?.customerId) {
sendJson(res, { customer: null });

const {
success,
reason,
didUpdate,
stripeCustomer: internalCustomer,
} = await stripeService.synchronizeStripeWithJetstreamIfRequired({ userId: user.id });
if (!success) {
logger.error({ userId: user.id }, `Did not synchronize Stripe with Jetstream: ${reason}`);
sendJson(res, { customer: null, didUpdate });
return;
}
if (!internalCustomer) {
sendJson(res, { customer: null, didUpdate });
return;
}
const customer = stripeService.convertCustomerWithSubscriptionsToUserFacing(internalCustomer);

let userProfile: UserProfileUi | undefined;
if (didUpdate) {
userProfile = await userDbService.findIdByUserIdUserFacing({ userId: user.id });
}

const customer = await stripeService.getUserFacingStripeCustomer({ customerId: userProfile.billingAccount.customerId });
sendJson(res, { customer });
sendJson(res, { customer, didUpdate, userProfile });
});

const createBillingPortalSession = createRoute(
Expand Down
118 changes: 118 additions & 0 deletions apps/api/src/app/controllers/data-sync.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { ensureBoolean, REGEX } from '@jetstream/shared/utils';
import { SyncRecordOperationSchema } from '@jetstream/types';
import { parseISO } from 'date-fns';
import { clamp } from 'lodash';
import { z } from 'zod';
import * as userSyncDbService from '../db/data-sync.db';
import { emitRecordSyncEventsToOtherClients, SyncEvent } from '../services/data-sync-broadcast.service';
import { sendJson } from '../utils/response.handlers';
import { createRoute } from '../utils/route.utils';

// FIXME: TEMPORARY UNTIL ALL CLIENTS HAVE BEEN BACKFILLED
export const SyncRecordOperationSchemaFillHashedKey = z
.object({
key: z.string(),
hashedKey: z.string().optional(),
data: z.record(z.unknown()),
})
.passthrough()
.array()
.transform((records) => {
return SyncRecordOperationSchema.array()
.max(userSyncDbService.MAX_SYNC)
.parse(
records.map((record) => {
if (!record.hashedKey) {
record.hashedKey = userSyncDbService.hashRecordSyncKey(record.key);
}
if (!(record as any).data?.hashedKey) {
(record as any).data.hashedKey = record.hashedKey;
}
return record;
})
);
});

export const routeDefinition = {
pull: {
controllerFn: () => pull,
validators: {
query: z.object({
updatedAt: z
.string()
.regex(REGEX.ISO_DATE)
.nullish()
.transform((val) => (val ? parseISO(val) : null)),
limit: z.coerce
.number()
.int()
.optional()
.default(userSyncDbService.MAX_PULL)
.transform((val) => clamp(val, userSyncDbService.MIN_PULL, userSyncDbService.MAX_PULL)),
/**
* Used for pagination, if there are more records, this is the last key of the previous page
*/
lastKey: z.string().nullish(),
}),
hasSourceOrg: false,
},
},
push: {
controllerFn: () => push,
validators: {
query: z.object({
clientId: z.string().uuid(),
updatedAt: z
.string()
.regex(REGEX.ISO_DATE)
.nullish()
.transform((val) => (val ? parseISO(val) : null)),
includeAllIfUpdatedAtNull: z
.union([z.enum(['true', 'false']), z.boolean()])
.optional()
.default(false)
.transform(ensureBoolean),
}),
body: SyncRecordOperationSchemaFillHashedKey,
// Original code:
// body: SyncRecordOperationSchema.array().max(userSyncDbService.MAX_SYNC),
hasSourceOrg: false,
},
},
};

/**
* Pull changes from server
*/
const pull = createRoute(routeDefinition.pull.validators, async ({ user, query }, req, res) => {
const { lastKey, updatedAt, limit } = query;
const response = await userSyncDbService.findByUpdatedAt({
userId: user.id,
lastKey,
updatedAt,
limit,
});
sendJson(res, response);
});

/**
* Push changes to server and emit to any other clients the user has active
*/
const push = createRoute(routeDefinition.push.validators, async ({ user, body: records, query }, req, res) => {
const response = await userSyncDbService.syncRecordChanges({
updatedAt: query.updatedAt,
userId: user.id,
records,
includeAllIfUpdatedAtNull: query.includeAllIfUpdatedAtNull,
});

const syncEvent: SyncEvent = {
clientId: query.clientId,
data: { hashedKeys: response.records.map(({ hashedKey }) => hashedKey) },
userId: user.id,
};

emitRecordSyncEventsToOtherClients(req.session.id, syncEvent);

sendJson(res, response);
});
Loading

0 comments on commit bb88cdb

Please sign in to comment.