@nuxtagentic/google 0.0.0-stage → 0.1.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.md ADDED
@@ -0,0 +1,9 @@
1
+ Proprietary software. All rights reserved.
2
+
3
+ Use of Nuxt Agentic packages and starter materials is governed by the applicable
4
+ commercial agreement supplied with a purchase. This repository does not grant
5
+ an open-source license or permission to redistribute the packages as a competing
6
+ template or module library.
7
+
8
+ Third-party dependencies retain their respective licenses. This notice does not
9
+ replace or modify those licenses.
package/README.md CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # Google connections
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `@nuxtagentic/google` owns the shared Google connection lifecycle and authenticated
4
+ server access. It installs Identity and Prompt. Include `googleModels` from
5
+ `@nuxtagentic/google/schema` in the host config to store service preferences.
6
+
7
+ Workspace features use these Google connections. Integrations (Gmail, Calendar,
8
+ Drive, Analytics, Search Console, Ads) connect through CloudConfig Connect instead,
9
+ with a Google grant separate from sign-in. This shared service remains in the agentic
10
+ starter even though the standalone Calendar, Files, and Emails dashboard modules have
11
+ moved out.
12
+
13
+ Configure the Google OAuth client with `pnpm exec cloudconfig auth add google`.
14
+ Register `/api/auth/callback/google` for each app origin. Account linking expects
15
+ the same Google email as the signed-in account. Enable the Google provider APIs
16
+ required by the integration actions you choose, such as Gmail, Calendar, or Drive.
17
+ OAuth client secrets and refresh tokens stay on the server.
18
+
19
+ Use [Integrations](../integrations/README.md) to connect Google accounts for agents,
20
+ choose permission scopes and actions, and [Automations](../automations/README.md) to
21
+ configure triggers. Connecting a service requests its permissions without replacing the
22
+ sign-in session; disconnecting disables the service while retaining Google sign-in.
package/module.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { createResolver, defineNuxtModule } from '@nuxt/kit'
2
+ import { registerWorkspaceModule } from '@nuxtagentic/kit'
3
+
4
+ export default defineNuxtModule({
5
+ meta: { name: '@nuxtagentic/google' },
6
+ moduleDependencies: {
7
+ '@nuxtagentic/identity': {},
8
+ '@nuxtagentic/prompt': {},
9
+ },
10
+ setup(_options, nuxt) {
11
+ const resolver = createResolver(import.meta.url)
12
+ registerWorkspaceModule(
13
+ {
14
+ id: 'google',
15
+ runtime: resolver.resolve('./runtime'),
16
+ metadata: resolver.resolve('./runtime/shared/agent.ts'),
17
+ },
18
+ nuxt,
19
+ )
20
+ },
21
+ })
package/package.json CHANGED
@@ -1,6 +1,46 @@
1
1
  {
2
2
  "name": "@nuxtagentic/google",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "files": [
6
+ "module.ts",
7
+ "runtime",
8
+ "schema.ts",
9
+ "README.md",
10
+ "LICENSE.md"
11
+ ],
12
+ "exports": {
13
+ ".": "./module.ts",
14
+ "./package.json": "./package.json",
15
+ "./schema": "./schema.ts",
16
+ "./runtime/*.vue": "./runtime/*.vue",
17
+ "./runtime/*.css": "./runtime/*.css",
18
+ "./runtime/*.json": "./runtime/*.json",
19
+ "./runtime/*.svg": "./runtime/*.svg",
20
+ "./runtime/*": "./runtime/*.ts"
21
+ },
22
+ "dependencies": {
23
+ "@cloudconfig/core": "0.2.0-beta.2",
24
+ "@nuxt/kit": "4.6.1",
25
+ "h3": "1.15.11",
26
+ "@nuxtagentic/prompt": "0.1.0",
27
+ "@nuxtagentic/kit": "0.1.0",
28
+ "@nuxtagentic/identity": "0.1.0"
29
+ },
30
+ "peerDependencies": {
31
+ "nuxt": "^4.6.1",
32
+ "vue": "^3.5.43"
33
+ },
34
+ "cloudconfig": {
35
+ "models": "googleModels"
36
+ },
37
+ "license": "SEE LICENSE IN LICENSE.md",
38
+ "description": "Google connection lifecycle for Nuxt Agentic.",
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "engines": {
44
+ "node": "^22.22.3 || ^24.15.0 || >=26.0.0"
45
+ }
6
46
  }
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts">
2
+ import { useGoogleConnection } from '../composables/useGoogleConnection'
3
+ import { useRoute } from '#imports'
4
+ import { ref } from 'vue'
5
+
6
+ import { getIntegrationErrorMessage, type GoogleService } from '../shared/google'
7
+
8
+ const { service, name, icon, description } = defineProps<{
9
+ service: GoogleService
10
+ name: string
11
+ icon: string
12
+ description: string
13
+ }>()
14
+ const connection = useGoogleConnection(() => service)
15
+ const route = useRoute()
16
+ const error = ref('')
17
+ async function disconnect() {
18
+ error.value = ''
19
+ try {
20
+ await connection.disconnect()
21
+ } catch (cause) {
22
+ error.value = getIntegrationErrorMessage(cause)
23
+ }
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <SettingsSection
29
+ :title="name"
30
+ :description="description"
31
+ >
32
+ <div class="flex items-center gap-3 py-2">
33
+ <UIcon
34
+ :name="icon"
35
+ class="size-5 shrink-0 text-muted"
36
+ />
37
+ <p class="min-w-0 flex-1 text-[13px] text-muted">
38
+ {{
39
+ connection.connected.value ? connection.data.value?.email : 'Connect your Google account.'
40
+ }}
41
+ </p>
42
+ <UDropdownMenu
43
+ v-if="connection.connected.value"
44
+ :items="[
45
+ {
46
+ label: 'Reconnect',
47
+ icon: 'i-lucide-refresh-cw',
48
+ onSelect: () => connection.connect('/dashboard/settings#connections'),
49
+ },
50
+ { label: 'Disconnect', icon: 'i-lucide-unplug', onSelect: disconnect },
51
+ ]"
52
+ >
53
+ <UButton
54
+ label="Connected"
55
+ trailing-icon="i-lucide-chevron-down"
56
+ variant="outline"
57
+ size="sm"
58
+ :loading="connection.connecting.value"
59
+ />
60
+ </UDropdownMenu>
61
+ <UButton
62
+ v-else
63
+ :label="connection.data.value?.available === false ? 'Invite only' : 'Connect'"
64
+ size="sm"
65
+ :disabled="
66
+ connection.data.value?.available === false || connection.data.value?.configured === false
67
+ "
68
+ :loading="connection.connecting.value"
69
+ @click="connection.connect('/dashboard/settings#connections')"
70
+ />
71
+ </div>
72
+ <p
73
+ v-if="error"
74
+ role="alert"
75
+ class="text-sm text-error"
76
+ >
77
+ {{ error }}
78
+ </p>
79
+ <p
80
+ v-if="route.query.googleError === service"
81
+ role="alert"
82
+ class="text-sm text-error"
83
+ >
84
+ Google could not connect this account. Use the Google account matching your sign-in email and
85
+ grant the requested permissions.
86
+ </p>
87
+ <slot />
88
+ </SettingsSection>
89
+ </template>
@@ -0,0 +1,112 @@
1
+ import { useCurrentUser } from '@nuxtagentic/identity/runtime/composables/useCurrentUser'
2
+ import {
3
+ useCloudConfigAuth,
4
+ useRequestFetch,
5
+ useRoute,
6
+ useToast,
7
+ useAsyncData,
8
+ clearNuxtData,
9
+ useState,
10
+ } from '#imports'
11
+ import { ref, computed, toValue, type MaybeRefOrGetter } from 'vue'
12
+ import {
13
+ type GoogleService,
14
+ type GoogleConnection,
15
+ getIntegrationErrorMessage,
16
+ } from '../shared/google'
17
+ import { GOOGLE_SERVICES } from '#workspace/client'
18
+
19
+ export function useGoogleConnection(service: MaybeRefOrGetter<GoogleService>) {
20
+ const settings = computed(() => {
21
+ const serviceId = toValue(service)
22
+ const configuration = GOOGLE_SERVICES[serviceId]
23
+ if (!configuration) {
24
+ throw new Error(`Google connection "${serviceId}" is not enabled in this application.`)
25
+ }
26
+ return { ...configuration, serviceId }
27
+ })
28
+ const user = useCurrentUser()
29
+ const auth = useCloudConfigAuth()
30
+ const fetch = useRequestFetch()
31
+ const route = useRoute()
32
+ const toast = useToast()
33
+ const connecting = ref(false)
34
+ const resource = useAsyncData(
35
+ computed(() => `google:${settings.value.serviceId}:connection:${user.value?.id ?? ''}`),
36
+ () =>
37
+ user.value
38
+ ? fetch<GoogleConnection>(`/api/integrations/google/${settings.value.serviceId}/status`)
39
+ : Promise.resolve<GoogleConnection>({
40
+ configured: false,
41
+ connected: false,
42
+ needsConsent: true,
43
+ }),
44
+ { lazy: true, deep: false, dedupe: 'defer' },
45
+ )
46
+ const connected = computed(() => resource.data.value?.connected === true && !resource.error.value)
47
+
48
+ async function connect(callbackURL = route.fullPath) {
49
+ const { serviceId: selectedService, scopes: requestedScopes } = settings.value
50
+ const reauthorize = connected.value
51
+ connecting.value = true
52
+ try {
53
+ const enabled = await fetch<{ connected: boolean }>(
54
+ `/api/integrations/google/${selectedService}/connection`,
55
+ { method: 'POST', body: { enabled: true } },
56
+ )
57
+ if (enabled.connected && !reauthorize && !resource.error.value) {
58
+ await resource.refresh()
59
+ return true
60
+ }
61
+ const accounts = await auth.listAccounts()
62
+ if (accounts.error) {
63
+ throw new Error(accounts.error.message)
64
+ }
65
+ const scopes = [
66
+ ...new Set([
67
+ ...(accounts.data?.find((account) => account.providerId === 'google')?.scopes ?? []),
68
+ ...requestedScopes,
69
+ ]),
70
+ ]
71
+ const result = await auth.linkSocial({
72
+ provider: 'google',
73
+ scopes,
74
+ callbackURL,
75
+ errorCallbackURL: `/dashboard/settings?googleError=${selectedService}#connections`,
76
+ additionalParams: {
77
+ prompt: 'consent',
78
+ access_type: 'offline',
79
+ include_granted_scopes: 'true',
80
+ },
81
+ })
82
+ if (result.error) {
83
+ throw new Error(result.error.message ?? 'Could not connect Google.')
84
+ }
85
+ return false
86
+ } catch (cause) {
87
+ toast.add({
88
+ title: 'Could not connect Google',
89
+ description: getIntegrationErrorMessage(cause),
90
+ color: 'error',
91
+ })
92
+ return false
93
+ } finally {
94
+ connecting.value = false
95
+ }
96
+ }
97
+
98
+ async function disconnect() {
99
+ const { serviceId: selectedService, disconnect: cleanup } = settings.value
100
+ await fetch(`/api/integrations/google/${selectedService}/connection`, {
101
+ method: 'POST',
102
+ body: { enabled: false },
103
+ })
104
+ await resource.refresh()
105
+ const prefixes = cleanup?.dataPrefixes ?? []
106
+ clearNuxtData((key) => prefixes.some((prefix) => key.startsWith(prefix)))
107
+ for (const prefix of cleanup?.userStatePrefixes ?? []) {
108
+ useState(`${prefix}${user.value?.id ?? ''}`, () => null).value = null
109
+ }
110
+ }
111
+ return { ...resource, connected, connecting, connect, disconnect }
112
+ }
@@ -0,0 +1,43 @@
1
+ import { createError, defineEventHandler, getRouterParam, readBody } from 'h3'
2
+ import { googleDisconnectHooks } from '#workspace/server'
3
+ import {
4
+ getGoogleIntegration,
5
+ assertGoogleMutation,
6
+ getGoogleService,
7
+ } from '../../../../utils/google'
8
+
9
+ export default defineEventHandler(async (event) => {
10
+ assertGoogleMutation(event)
11
+ const service = getGoogleService(getRouterParam(event, 'service'))
12
+ const body = await readBody<{ enabled?: boolean }>(event)
13
+ if (typeof body?.enabled !== 'boolean') {
14
+ throw createError({
15
+ statusCode: 400,
16
+ statusMessage: 'Choose whether to enable this connection.',
17
+ })
18
+ }
19
+ const { enabled } = body
20
+ const { available, unavailableReason, user, record, api, granted } = await getGoogleIntegration(
21
+ event,
22
+ service,
23
+ )
24
+ if (enabled && !available) {
25
+ throw createError({ statusCode: 403, statusMessage: unavailableReason })
26
+ }
27
+ const input = { enabled, ownerId: user.id }
28
+ if (record) {
29
+ await api.googleIntegration.update(record.id, input)
30
+ } else {
31
+ await api.googleIntegration.create({
32
+ ...input,
33
+ key: `${user.id}:${service}`,
34
+ service,
35
+ })
36
+ }
37
+ if (!enabled) {
38
+ for (const disconnect of googleDisconnectHooks) {
39
+ await disconnect({ event, service, user })
40
+ }
41
+ }
42
+ return { connected: enabled && granted }
43
+ })
@@ -0,0 +1,21 @@
1
+ import { defineEventHandler, getRouterParam } from 'h3'
2
+ import {
3
+ getGoogleService,
4
+ getGoogleIntegration,
5
+ isGoogleConfigured,
6
+ } from '../../../../utils/google'
7
+ export default defineEventHandler(async (event) => {
8
+ const service = getGoogleService(getRouterParam(event, 'service'))
9
+ const { connected, granted, available, account, user } = await getGoogleIntegration(
10
+ event,
11
+ service,
12
+ )
13
+ return {
14
+ configured: isGoogleConfigured(event),
15
+ connected,
16
+ needsConsent: !granted,
17
+ available,
18
+ accountId: account?.accountId,
19
+ email: user.email,
20
+ }
21
+ })
@@ -0,0 +1,222 @@
1
+ import { createError, getHeader, getRequestURL, setHeader, type H3Event } from 'h3'
2
+ import { useRuntimeConfig } from '#imports'
3
+ import { useCloudConfigServer } from '#cloudconfig/server'
4
+ import type { GoogleConfig } from '@nuxtagentic/google/schema'
5
+ import type { GoogleService } from '../../shared/google'
6
+ import { GOOGLE_SERVICES, googleAvailabilityHooks, getWorkspaceMetadata } from '#workspace/server'
7
+
8
+ interface GoogleAccount {
9
+ id: string
10
+ accountId: string
11
+ providerId: string
12
+ scopes: string[]
13
+ }
14
+
15
+ export function getGoogleAuthPath(event: H3Event) {
16
+ return (
17
+ (useRuntimeConfig(event).public.cloudconfig as { authBasePath?: string })?.authBasePath ??
18
+ '/api/auth'
19
+ )
20
+ }
21
+
22
+ export function isGoogleConfigured(event: H3Event) {
23
+ const env = event.context.cloudflare?.env as Record<string, unknown> | undefined
24
+ return !!(
25
+ (env?.GOOGLE_CLIENT_ID || process.env.GOOGLE_CLIENT_ID) &&
26
+ (env?.GOOGLE_CLIENT_SECRET || process.env.GOOGLE_CLIENT_SECRET)
27
+ )
28
+ }
29
+
30
+ export async function getGoogleContext(event: H3Event) {
31
+ const cached = event.context.googleConnectionContext as
32
+ ReturnType<typeof createGoogleContext> | undefined
33
+ if (cached) {
34
+ return cached
35
+ }
36
+ const context = createGoogleContext(event)
37
+ event.context.googleConnectionContext = context
38
+ return context
39
+ }
40
+
41
+ async function createGoogleContext(event: H3Event) {
42
+ setHeader(event, 'Cache-Control', 'private, no-store')
43
+ const session = await event.$fetch<{ user?: { id: string; email: string } } | null>(
44
+ `${getGoogleAuthPath(event)}/get-session`,
45
+ { query: { disableCookieCache: true } },
46
+ )
47
+ if (!session?.user) {
48
+ throw createError({ statusCode: 401, statusMessage: 'Sign in to connect Google.' })
49
+ }
50
+ const accounts = await event.$fetch<GoogleAccount[]>(`${getGoogleAuthPath(event)}/list-accounts`)
51
+ const api = await useCloudConfigServer<GoogleConfig>(event)
52
+ return {
53
+ api,
54
+ user: { ...session.user, id: Number(session.user.id) },
55
+ account: accounts.find((account) => account.providerId === 'google'),
56
+ }
57
+ }
58
+
59
+ export async function getGoogleIntegration(event: H3Event, service: GoogleService) {
60
+ const settings = getWorkspaceMetadata(event.context.workspaceModules as string[] | undefined)
61
+ .GOOGLE_SERVICES[service]
62
+ if (!settings) {
63
+ throw createError({ statusCode: 400, statusMessage: 'Choose an enabled Google connection.' })
64
+ }
65
+ const context = await getGoogleContext(event)
66
+ const rows = await context.api.googleIntegration.list({
67
+ pageSize: 1,
68
+ filters: {
69
+ type: 'group',
70
+ logic: 'and',
71
+ children: [
72
+ { type: 'condition', field: 'key', operator: 'eq', value: `${context.user.id}:${service}` },
73
+ ],
74
+ },
75
+ })
76
+ const record = rows.data[0]
77
+ const granted =
78
+ !!context.account && settings.scopes.every((scope) => context.account!.scopes.includes(scope))
79
+ let unavailableReason: string | undefined
80
+ for (const checkAvailability of googleAvailabilityHooks) {
81
+ unavailableReason = await checkAvailability({ event, service, user: context.user })
82
+ if (unavailableReason) {
83
+ break
84
+ }
85
+ }
86
+ const available = !unavailableReason
87
+ return {
88
+ ...context,
89
+ record,
90
+ granted,
91
+ available,
92
+ unavailableReason,
93
+ connected: available && granted && record?.enabled !== false,
94
+ }
95
+ }
96
+
97
+ export async function getGoogleAccessToken(event: H3Event, accountId: string) {
98
+ const tokens = (event.context.googleAccessTokens ??= new Map<string, Promise<string>>()) as Map<
99
+ string,
100
+ Promise<string>
101
+ >
102
+ const cached = tokens.get(accountId)
103
+ if (cached) {
104
+ return cached
105
+ }
106
+ const token = fetchGoogleAccessToken(event, accountId)
107
+ tokens.set(accountId, token)
108
+ return token
109
+ }
110
+
111
+ async function fetchGoogleAccessToken(event: H3Event, accountId: string) {
112
+ try {
113
+ const result = await event.$fetch<{ accessToken: string }>(
114
+ `${getGoogleAuthPath(event)}/get-access-token`,
115
+ { method: 'POST', body: { accountId } },
116
+ )
117
+ if (!result.accessToken) {
118
+ throw new Error('Missing token')
119
+ }
120
+ return result.accessToken
121
+ } catch {
122
+ throw createError({
123
+ statusCode: 401,
124
+ statusMessage: 'Your Google connection expired. Reconnect to continue.',
125
+ })
126
+ }
127
+ }
128
+
129
+ export async function getGoogleClient(event: H3Event, service: GoogleService) {
130
+ const context = await getGoogleIntegration(event, service)
131
+ if (!context.available) {
132
+ throw createError({ statusCode: 403, statusMessage: context.unavailableReason })
133
+ }
134
+ if (!context.connected || !context.account) {
135
+ throw createError({
136
+ statusCode: 409,
137
+ statusMessage: `Connect ${GOOGLE_SERVICES[service]?.label ?? service} to continue.`,
138
+ })
139
+ }
140
+ const accessToken = await getGoogleAccessToken(event, context.account.id)
141
+ async function request<T>(
142
+ url: string,
143
+ options: {
144
+ method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
145
+ body?: Record<string, unknown> | string
146
+ query?: Record<string, string | number | boolean | string[]>
147
+ headers?: Record<string, string>
148
+ responseType?: 'json' | 'arrayBuffer' | 'text'
149
+ } = {},
150
+ ): Promise<T> {
151
+ try {
152
+ return (await $fetch<T>(url, {
153
+ ...options,
154
+ headers: { Authorization: `Bearer ${accessToken}`, ...options.headers },
155
+ retry: 0,
156
+ timeout: 30_000,
157
+ })) as T
158
+ } catch (cause) {
159
+ const error = cause as { statusCode?: number; status?: number }
160
+ const status = error.statusCode ?? error.status ?? 502
161
+ const message = googleErrorMessages[status] ?? 'Could not reach Google. Please try again.'
162
+ throw createError({ statusCode: status, statusMessage: message })
163
+ }
164
+ }
165
+ return { ...context, request }
166
+ }
167
+
168
+ const googleErrorMessages: Record<number, string> = {
169
+ 401: 'Your Google connection expired. Reconnect to continue.',
170
+ 403: 'Google denied access. Check the account permissions and reconnect.',
171
+ 404: 'This item is no longer available in Google.',
172
+ 412: 'This item changed in Google. Refresh it before saving.',
173
+ 429: 'Google is busy. Wait a moment and try again.',
174
+ }
175
+
176
+ export function assertGoogleMutation(event: H3Event) {
177
+ const origin = getHeader(event, 'origin')
178
+ if (origin && origin !== getRequestURL(event).origin) {
179
+ throw createError({ statusCode: 403, statusMessage: 'Open Google actions from this app.' })
180
+ }
181
+ }
182
+
183
+ export function getGoogleService(value: unknown): GoogleService {
184
+ if (typeof value !== 'string' || !Object.hasOwn(GOOGLE_SERVICES, value)) {
185
+ throw createError({ statusCode: 400, statusMessage: 'Choose a valid Google connection.' })
186
+ }
187
+ return value as GoogleService
188
+ }
189
+
190
+ export function assertGoogleId(value: unknown) {
191
+ if (typeof value !== 'string' || !/^[\w-]{1,2048}$/.test(value)) {
192
+ throw createError({ statusCode: 400, statusMessage: 'Choose a valid Google item.' })
193
+ }
194
+ return value
195
+ }
196
+
197
+ /** The unique key index also bounds user-owned job listings. */
198
+ export function getGoogleOwnerKeys(userId: number) {
199
+ return [
200
+ { type: 'condition' as const, field: 'key', operator: 'gte' as const, value: `${userId}:` },
201
+ { type: 'condition' as const, field: 'key', operator: 'lt' as const, value: `${userId};` },
202
+ ]
203
+ }
204
+
205
+ /** Keep large pages from issuing a burst of Google requests. */
206
+ export async function mapWithConcurrency<T, R>(
207
+ items: T[],
208
+ map: (item: T) => Promise<R>,
209
+ concurrency = 4,
210
+ ): Promise<R[]> {
211
+ const results = new Array<R>(items.length)
212
+ let cursor = 0
213
+ await Promise.all(
214
+ Array.from({ length: Math.min(concurrency, items.length) }, async () => {
215
+ while (cursor < items.length) {
216
+ const index = cursor++
217
+ results[index] = await map(items[index]!)
218
+ }
219
+ }),
220
+ )
221
+ return results
222
+ }
@@ -0,0 +1,5 @@
1
+ import type { WorkspaceMetadata } from '@nuxtagentic/prompt/server-contributions'
2
+
3
+ export default {
4
+ excludedModels: ['googleIntegration'],
5
+ } satisfies WorkspaceMetadata
@@ -0,0 +1,13 @@
1
+ export type GoogleService = 'drive' | 'calendar' | 'gmail'
2
+ export interface GoogleConnection {
3
+ configured: boolean
4
+ connected: boolean
5
+ needsConsent: boolean
6
+ available?: boolean
7
+ accountId?: string
8
+ email?: string
9
+ }
10
+ export function getIntegrationErrorMessage(cause: unknown) {
11
+ const error = cause as { data?: { statusMessage?: string; message?: string }; message?: string }
12
+ return error?.data?.statusMessage ?? error?.data?.message ?? error?.message ?? 'Please try again.'
13
+ }
package/schema.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { model, t } from '@cloudconfig/core'
2
+
3
+ export const googleModels = {
4
+ googleIntegration: model(
5
+ {
6
+ key: t.text().required().unique().serverOnly(),
7
+ service: t.select(['drive', 'calendar', 'gmail']).required(),
8
+ enabled: t.boolean().default(true).serverOnly(),
9
+ settings: t.json().serverOnly(),
10
+ ownerId: t.number().required().serverOnly(),
11
+ },
12
+ {
13
+ access: {
14
+ list: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
15
+ read: ({ user, where }) => (user ? where('ownerId', 'eq', user.id) : false),
16
+ create: ({ user, input }) => !!user && input?.ownerId === user.id,
17
+ update: ({ user, input, where }) =>
18
+ user && input?.ownerId === user.id ? where('ownerId', 'eq', user.id) : false,
19
+ },
20
+ },
21
+ ),
22
+ }
23
+
24
+ export type GoogleConfig = { models: typeof googleModels }