mirror of
https://fast.feibisi.com/https://github.com/parcelvoy/platform.git
synced 2025-09-01 12:26:08 +08:00
feat: allow for having internal subscription types
This commit is contained in:
parent
b42d022923
commit
ec165ba72e
12 changed files with 92 additions and 14 deletions
|
@ -0,0 +1,11 @@
|
|||
exports.up = async function(knex) {
|
||||
await knex.schema.table('subscriptions', function(table) {
|
||||
table.tinyint('is_public').defaultTo(1)
|
||||
})
|
||||
}
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.table('subscriptions', function(table) {
|
||||
table.dropColumn('is_public')
|
||||
})
|
||||
}
|
|
@ -59,7 +59,7 @@ export type SegmentPostEvent = {
|
|||
context: Record<string, any> & SegmentContext
|
||||
properties: Record<string, any>
|
||||
traits?: Record<string, any>
|
||||
type: 'track' | 'alias' | 'identify' | 'device'
|
||||
type: 'track' | 'alias' | 'identify' | 'device' | 'unsubscribe'
|
||||
timestamp: string
|
||||
} & (
|
||||
{
|
||||
|
@ -74,6 +74,10 @@ export type SegmentPostEvent = {
|
|||
type: 'device'
|
||||
properties: Record<string, any>
|
||||
}
|
||||
| {
|
||||
type: 'unsubscribe',
|
||||
properties: Record<string, any>
|
||||
}
|
||||
)
|
||||
|
||||
export type SegmentPostEventsRequest = SegmentPostEvent[]
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Job } from '../queue'
|
|||
import { parseLocale } from '../utilities'
|
||||
import UserAliasJob from '../users/UserAliasJob'
|
||||
import UserDeviceJob from '../users/UserDeviceJob'
|
||||
import UnsubscribeJob from '../subscriptions/UnsubscribeJob'
|
||||
|
||||
const router = new Router<ProjectState>()
|
||||
router.use(projectMiddleware)
|
||||
|
@ -65,7 +66,7 @@ const segmentEventsRequest: JSONSchemaType<SegmentPostEventsRequest> = {
|
|||
],
|
||||
},
|
||||
minItems: 1,
|
||||
maxItems: 1000,
|
||||
maxItems: 2000,
|
||||
} as any
|
||||
router.post('/segment', async ctx => {
|
||||
const events = validate(segmentEventsRequest, ctx.request.body)
|
||||
|
@ -115,6 +116,12 @@ router.post('/segment', async ctx => {
|
|||
...identity,
|
||||
...event.properties as any,
|
||||
}))
|
||||
} else if (event.type === 'unsubscribe') {
|
||||
|
||||
chunks.push(UnsubscribeJob.from({
|
||||
...identity,
|
||||
...event.properties as any,
|
||||
}))
|
||||
}
|
||||
|
||||
// Based on queue max batch size, process in largest chunks
|
||||
|
|
|
@ -25,6 +25,7 @@ import ScheduledEntranceJob from '../journey/ScheduledEntranceJob'
|
|||
import ScheduledEntranceOrchestratorJob from '../journey/ScheduledEntranceOrchestratorJob'
|
||||
import CampaignAbortJob from '../campaigns/CampaignAbortJob'
|
||||
import MigrateJob from '../organizations/MigrateJob'
|
||||
import UnsubscribeJob from '../subscriptions/UnsubscribeJob'
|
||||
|
||||
export const jobs = [
|
||||
CampaignAbortJob,
|
||||
|
@ -45,6 +46,7 @@ export const jobs = [
|
|||
ScheduledEntranceJob,
|
||||
ScheduledEntranceOrchestratorJob,
|
||||
TextJob,
|
||||
UnsubscribeJob,
|
||||
UpdateJourneysJob,
|
||||
UserAliasJob,
|
||||
UserDeleteJob,
|
||||
|
|
|
@ -5,6 +5,7 @@ export default class Subscription extends Model {
|
|||
project_id!: number
|
||||
name!: string
|
||||
channel!: ChannelType
|
||||
is_public!: boolean
|
||||
}
|
||||
|
||||
export enum SubscriptionState {
|
||||
|
@ -21,4 +22,4 @@ export type UserSubscription = {
|
|||
}
|
||||
|
||||
export type SubscriptionParams = Omit<Subscription, ModelParams>
|
||||
export type SubscriptionUpdateParams = Pick<SubscriptionParams, 'name'>
|
||||
export type SubscriptionUpdateParams = Pick<SubscriptionParams, 'name' | 'is_public'>
|
||||
|
|
|
@ -256,6 +256,10 @@ export const subscriptionCreateSchema: JSONSchemaType<SubscriptionParams> = {
|
|||
type: 'string',
|
||||
enum: ['email', 'text', 'push', 'webhook'],
|
||||
},
|
||||
is_public: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}
|
||||
|
@ -281,16 +285,20 @@ router.get('/:subscriptionId', async ctx => {
|
|||
export const subscriptionUpdateSchema: JSONSchemaType<SubscriptionUpdateParams> = {
|
||||
$id: 'subscriptionUpdate',
|
||||
type: 'object',
|
||||
required: ['name'],
|
||||
required: ['name', 'is_public'],
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
},
|
||||
is_public: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
}
|
||||
router.patch('/:subscriptionId', async ctx => {
|
||||
const payload = validate(subscriptionUpdateSchema, ctx.request.body)
|
||||
console.log(payload)
|
||||
ctx.body = await updateSubscription(ctx.state.subscription!.id, payload)
|
||||
})
|
||||
|
||||
|
|
|
@ -16,9 +16,13 @@ export const pagedSubscriptions = async (params: PageParams, projectId: number)
|
|||
)
|
||||
}
|
||||
|
||||
export const getUserSubscriptions = async (user: User, params?: PageParams): Promise<SearchResult<UserSubscription>> => {
|
||||
export const getUserSubscriptions = async (user: User, params?: PageParams, onlyPublic = true): Promise<SearchResult<UserSubscription>> => {
|
||||
const subscriptions = await Subscription.all(
|
||||
qb => qb.where('project_id', user.project_id),
|
||||
qb => {
|
||||
qb.where('project_id', user.project_id)
|
||||
if (onlyPublic) qb.where('is_public', true)
|
||||
return qb
|
||||
},
|
||||
)
|
||||
return {
|
||||
results: subscriptions.map(subscription => ({
|
||||
|
|
24
apps/platform/src/subscriptions/UnsubscribeJob.ts
Normal file
24
apps/platform/src/subscriptions/UnsubscribeJob.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Job } from '../queue'
|
||||
import { toggleSubscription } from './SubscriptionService'
|
||||
import { SubscriptionState } from './Subscription'
|
||||
import { getUserFromClientId } from '../users/UserRepository'
|
||||
|
||||
type UserUnsubscribeParams = {
|
||||
external_id: string
|
||||
project_id: number
|
||||
subscription_id: number
|
||||
}
|
||||
|
||||
export default class UnsubscribeJob extends Job {
|
||||
static $name = 'unsubscribe'
|
||||
|
||||
static from(data: UserUnsubscribeParams): UnsubscribeJob {
|
||||
return new this(data)
|
||||
}
|
||||
|
||||
static async handler({ project_id, subscription_id, external_id }: UserUnsubscribeParams) {
|
||||
const user = await getUserFromClientId(project_id, { external_id })
|
||||
if (!user) return
|
||||
await toggleSubscription(user.id, subscription_id, SubscriptionState.unsubscribed)
|
||||
}
|
||||
}
|
|
@ -177,7 +177,7 @@ router.get('/:userId/events', async ctx => {
|
|||
|
||||
router.get('/:userId/subscriptions', async ctx => {
|
||||
const params = extractQueryParams(ctx.query, searchParamsSchema)
|
||||
ctx.body = await getUserSubscriptions(ctx.state.user!, params)
|
||||
ctx.body = await getUserSubscriptions(ctx.state.user!, params, false)
|
||||
})
|
||||
|
||||
router.patch('/:userId/subscriptions', async ctx => {
|
||||
|
|
|
@ -227,6 +227,7 @@
|
|||
"missing": "Missing",
|
||||
"name": "Name",
|
||||
"new_team_member": "New Team Member",
|
||||
"no": "No",
|
||||
"no_providers": "No Providers",
|
||||
"no_template_alert_body": "There are no templates yet for this campaign. Add a locale above or use the button below to get started.",
|
||||
"now": "Now",
|
||||
|
@ -250,6 +251,8 @@
|
|||
"projects_description": "Projects are isolated workspaces with their own sets of users, events, lists, campaigns, and journeys.",
|
||||
"project_settings_saved": "Project settings saved!",
|
||||
"provider": "Provider",
|
||||
"public": "Public",
|
||||
"public_desc": "Should a user be able to see this channel in the preferences center?",
|
||||
"publish": "Publish",
|
||||
"published": "Published",
|
||||
"push": "Push",
|
||||
|
@ -385,5 +388,6 @@
|
|||
"wait": "Wait",
|
||||
"wait_until": "Wait until",
|
||||
"webhook": "Webhook",
|
||||
"welcome": "Welcome!"
|
||||
"welcome": "Welcome!",
|
||||
"yes": "Yes"
|
||||
}
|
|
@ -521,11 +521,12 @@ export interface Subscription {
|
|||
id: number
|
||||
name: string
|
||||
channel: ChannelType
|
||||
is_public: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
export type SubscriptionCreateParams = Pick<Subscription, 'name' | 'channel'>
|
||||
export type SubscriptionUpdateParams = Pick<SubscriptionCreateParams, 'name'>
|
||||
export type SubscriptionCreateParams = Pick<Subscription, 'name' | 'channel' | 'is_public'>
|
||||
export type SubscriptionUpdateParams = Pick<SubscriptionCreateParams, 'name' | 'is_public'>
|
||||
|
||||
export type ProviderGroup = 'email' | 'text' | 'push' | 'webhook'
|
||||
export interface Provider {
|
||||
|
|
|
@ -11,6 +11,7 @@ import Button from '../../ui/Button'
|
|||
import { PlusIcon } from '../../ui/icons'
|
||||
import { snakeToTitle } from '../../utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SwitchField from '../../ui/form/SwitchField'
|
||||
|
||||
export default function Subscriptions() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -29,6 +30,11 @@ export default function Subscriptions() {
|
|||
title: t('channel'),
|
||||
cell: ({ item }) => snakeToTitle(item.channel),
|
||||
},
|
||||
{
|
||||
key: 'is_public',
|
||||
title: t('public'),
|
||||
cell: ({ item }) => item.is_public ? t('yes') : t('no'),
|
||||
},
|
||||
]}
|
||||
itemKey={({ item }) => item.id}
|
||||
onSelectRow={(row) => setEditing(row)}
|
||||
|
@ -49,12 +55,12 @@ export default function Subscriptions() {
|
|||
open={Boolean(editing)}
|
||||
onClose={() => setEditing(null)}
|
||||
>
|
||||
{editing && <FormWrapper<Pick<Subscription, 'id' | 'name' | 'channel'>>
|
||||
onSubmit={async ({ id, name, channel }) => {
|
||||
{editing && <FormWrapper<Pick<Subscription, 'id' | 'name' | 'channel' | 'is_public'>>
|
||||
onSubmit={async ({ id, name, channel, is_public }) => {
|
||||
if (id) {
|
||||
await api.subscriptions.update(project.id, id, { name })
|
||||
await api.subscriptions.update(project.id, id, { name, is_public })
|
||||
} else {
|
||||
await api.subscriptions.create(project.id, { name, channel })
|
||||
await api.subscriptions.create(project.id, { name, channel, is_public })
|
||||
}
|
||||
await state.reload()
|
||||
setEditing(null)
|
||||
|
@ -70,6 +76,12 @@ export default function Subscriptions() {
|
|||
required
|
||||
label={t('name')}
|
||||
/>
|
||||
<SwitchField
|
||||
form={form}
|
||||
name="is_public"
|
||||
subtitle={t('public_desc')}
|
||||
label={t('public')}
|
||||
/>
|
||||
{!editing.id && <SingleSelect.Field
|
||||
form={form}
|
||||
name="channel"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue