diff --git a/.github/workflows/frontend-deploy.yml b/.github/workflows/frontend-deploy.yml index 73411c8..fc40d81 100644 --- a/.github/workflows/frontend-deploy.yml +++ b/.github/workflows/frontend-deploy.yml @@ -33,6 +33,7 @@ jobs: SITE_CONTACT_EMAIL: hello@giraffeql.com SITE_DISCORD_LINK: https://discord.gg/CpSWfub9y6 SITE_GITHUB_REPOSITORY_URL: https://github.com/big213/giraffeql.com + LOGO_HAS_LIGHT_VARIANT: - name: Deploy to Firebase uses: w9jds/firebase-action@master with: diff --git a/backend/deploy.md.example b/backend/deploy.md.example index 16dbe64..ae86a55 100644 --- a/backend/deploy.md.example +++ b/backend/deploy.md.example @@ -12,7 +12,7 @@ firebase functions:config:set pg.user="user" pg.password="password" pg.database= # set base config -firebase functions:config:set base.origins="https://example.com" +firebase functions:config:set base.origins="https://example.com" base.timeout_seconds="60" # set serveImage config diff --git a/backend/env.json.example b/backend/env.json.example index 240f48e..17b81c6 100644 --- a/backend/env.json.example +++ b/backend/env.json.example @@ -22,6 +22,7 @@ "cache_bucket": "giraffeql-boilerplate.appspot.com/cache" }, "base": { - "origins": "https://boilerplate.giraffeql.com" + "origins": "https://boilerplate.giraffeql.com", + "timeout_seconds": "60" } } diff --git a/backend/functions/src/schema/core/helpers/sql.ts b/backend/functions/src/schema/core/helpers/sql.ts index 76c9a34..9e510af 100644 --- a/backend/functions/src/schema/core/helpers/sql.ts +++ b/backend/functions/src/schema/core/helpers/sql.ts @@ -904,7 +904,8 @@ export async function updateTableRow( // handle set fields and convert to actual sql fields, if aliased const sqlFields = {}; for (const fieldname in sqlQuery.fields) { - const sqlOptions = currentTypeDef.definition.fields[fieldname].sqlOptions; + const sqlOptions = + currentTypeDef.definition.fields[fieldname]?.sqlOptions; if (!sqlOptions) throw new Error(`'${fieldname}' is not a sql field`); sqlFields[sqlOptions.field ?? fieldname] = sqlOptions.parseValue diff --git a/backend/functions/src/schema/core/helpers/typeDef.ts b/backend/functions/src/schema/core/helpers/typeDef.ts index 2c99e2a..a83124a 100644 --- a/backend/functions/src/schema/core/helpers/typeDef.ts +++ b/backend/functions/src/schema/core/helpers/typeDef.ts @@ -465,19 +465,42 @@ export function generateEnumField( export function generateKeyValueArray( params: { + name?: string; valueType?: GiraffeqlScalarType; allowNullValue?: boolean; } & GenerateFieldParams ) { const { + name, valueType = Scalars.string, allowNullValue = false, ...remainingParams } = params; + + let finalObjectName: string; + + // if name is defined, check if it is not already defined + if (name) { + finalObjectName = name; + if (inputTypeDefs.has(name)) { + throw new GiraffeqlInitializationError({ + message: `Input type with name: '${name}' already exists`, + }); + } + } else { + finalObjectName = "keyValueObject"; + let iteration = 0; + // if no name, generate an appropriate name by adding an incrementing number + while (inputTypeDefs.has(finalObjectName)) { + finalObjectName += String(iteration); + iteration++; + } + } + // generate the input type if not exists - if (!inputTypeDefs.has("keyValueObject")) { + if (!inputTypeDefs.has(finalObjectName)) { new GiraffeqlInputType({ - name: "keyValueObject", + name: finalObjectName, description: "Object Input with key and value properties", fields: { key: new GiraffeqlInputFieldType({ @@ -486,16 +509,17 @@ export function generateKeyValueArray( }), value: new GiraffeqlInputFieldType({ type: valueType, - required: !allowNullValue, + required: true, + allowNull: allowNullValue, }), }, }); } // generate the object type if not exists - if (!objectTypeDefs.has("keyValueObject")) { + if (!objectTypeDefs.has(finalObjectName)) { new GiraffeqlObjectType({ - name: "keyValueObject", + name: finalObjectName, description: "Object with key and value properties", fields: { key: { @@ -512,7 +536,7 @@ export function generateKeyValueArray( return generateArrayField({ allowNullElement: false, - type: new GiraffeqlObjectTypeLookup("keyValueObject"), + type: new GiraffeqlObjectTypeLookup(finalObjectName), ...remainingParams, }); } diff --git a/backend/functions/src/schema/core/services/normal.ts b/backend/functions/src/schema/core/services/normal.ts index 6d64a42..f3f00df 100644 --- a/backend/functions/src/schema/core/services/normal.ts +++ b/backend/functions/src/schema/core/services/normal.ts @@ -10,9 +10,11 @@ import { SqlOrderByObject, SqlSelectQuery, SqlSelectQueryObject, + SqlSumQuery, SqlUpdateQuery, SqlWhereFieldOperator, SqlWhereObject, + sumTableRows, updateTableRow, } from "../helpers/sql"; import { permissionsCheck } from "../helpers/permissions"; @@ -687,6 +689,19 @@ export class NormalService extends BaseService { return recordsCount; } + // sum a field for the records matching the criteria + async sumSqlRecord( + sqlQuery: Omit, + fieldPath?: string[] + ): Promise { + const sum = await sumTableRows({ + ...sqlQuery, + table: this.typename, + }); + + return sum; + } + async createSqlRecord( sqlQuery: Omit, fieldPath?: string[] diff --git a/backend/functions/src/schema/models/github/service.ts b/backend/functions/src/schema/models/github/service.ts index d1d7fa1..6d4f7d7 100644 --- a/backend/functions/src/schema/models/github/service.ts +++ b/backend/functions/src/schema/models/github/service.ts @@ -129,6 +129,7 @@ query { repository(name: "${env.github.repository}") { latestRelease { tagName + createdAt } } } @@ -136,7 +137,7 @@ query { } `); - return response.viewer.organization.repository.latestRelease?.tagName; + return response.viewer.organization.repository.latestRelease; } else { // if no organization specified, lookup repository directly const response = await sendGraphqlRequest(` @@ -145,13 +146,14 @@ query { repository(name: "${env.github.repository}") { latestRelease { tagName + createdAt } } } } `); - return response.viewer.repository.latestRelease?.tagName; + return response.viewer.repository.latestRelease; } } catch (err) { throw new GiraffeqlBaseError({ diff --git a/backend/functions/src/schema/models/user/rootResolver.ts b/backend/functions/src/schema/models/user/rootResolver.ts index 5881544..2b3e417 100644 --- a/backend/functions/src/schema/models/user/rootResolver.ts +++ b/backend/functions/src/schema/models/user/rootResolver.ts @@ -20,11 +20,12 @@ export default { allowNull: false, type: User.typeDefLookup, resolver: ({ req, fieldPath, args, query }) => { - if (!req.user?.id) throw new Error("Login required"); + if (!req.user) throw new Error("Login required"); + return User.getRecord({ req, fieldPath, - args: { id: req.user?.id }, + args: { id: req.user!.id }, query, isAdmin: true, }); diff --git a/backend/functions/src/schema/models/user/service.ts b/backend/functions/src/schema/models/user/service.ts index 75c44df..6c51bf7 100644 --- a/backend/functions/src/schema/models/user/service.ts +++ b/backend/functions/src/schema/models/user/service.ts @@ -216,12 +216,15 @@ export class UserService extends PaginatedService { // args should be validated already const validatedArgs = args; //check if record exists - const item = await this.getFirstSqlRecord({ - select: ["id", "role", "firebaseUid"], - where: { - id: data!.id, + const item = await this.getFirstSqlRecord( + { + select: ["id", "role", "firebaseUid"], + where: { + id: data!.id, + }, }, - }); + fieldPath + ); // make sure email field, if provided, matches the firebase user email if ("email" in validatedArgs) { @@ -260,10 +263,13 @@ export class UserService extends PaginatedService { // args should be validated already const validatedArgs = args; // check if record exists, get ID - const item = await this.getFirstSqlRecord({ - select: ["id", "role", "firebaseUid"], - where: validatedArgs.item, - }); + const item = await this.getFirstSqlRecord( + { + select: ["id", "role", "firebaseUid"], + where: validatedArgs.item, + }, + fieldPath + ); // convert any lookup/joined fields into IDs await this.handleLookupArgs(validatedArgs.fields, fieldPath); @@ -333,10 +339,13 @@ export class UserService extends PaginatedService { const validatedArgs = args; // confirm existence of item and get ID - const item = await this.getFirstSqlRecord({ - select: ["id", "firebaseUid"], - where: validatedArgs, - }); + const item = await this.getFirstSqlRecord( + { + select: ["id", "firebaseUid"], + where: validatedArgs, + }, + fieldPath + ); // first, fetch the requested query, if any const requestedResults = this.isEmptyQuery(query) diff --git a/frontend/.env.example b/frontend/.env.example index 8ccdabb..550df89 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -2,7 +2,7 @@ VER=DEV #required SITE_NAME="Giraffeql Boilerplate" -SITE_DESCRIPTION="Giraffeql Boilerplate is a basic boilerplate site for showcasing the Giraffeql API querying language in a basic and extendable implementation." +SITE_DESCRIPTION="Giraffeql Boilerplate is a basic boilerplate site for showcasing the Giraffeql API querying language in a basic and extendable implementation" #optional SITE_IMAGE_URL=https://cdn.giraffeql.com/permanent/android-chrome-512x512.png @@ -10,6 +10,10 @@ SITE_CONTACT_EMAIL=hello@giraffeql.com SITE_DISCORD_LINK=https://discord.gg/CpSWfub9y6 SITE_GITHUB_REPOSITORY_URL=https://github.com/big213/giraffeql-boilerplate +#logo +#if this is enabled, must have a variant for logo-vertical-light.png and logo-horizontal-light.png in static folder +#LOGO_HAS_LIGHT_VARIANT=true + #dev API_URL=http://localhost:5001/giraffeql-boilerplate/us-central1/api IMAGE_SERVING_URL=https://cdn.giraffeql.com diff --git a/frontend/components/common/versionCheckText.vue b/frontend/components/common/versionCheckText.vue index e6342e4..91b6cde 100644 --- a/frontend/components/common/versionCheckText.vue +++ b/frontend/components/common/versionCheckText.vue @@ -2,14 +2,14 @@
{{ getBuildInfo() }} mdi-sync-alert Refresh - + Close @@ -44,10 +44,11 @@ import 'firebase/auth' export default { data() { return { - open: false, + snackbarStatus: false, currentVersion: null, latestVersion: null, - hasNewerVersion: false, + + showNewerVersionIcon: false, } }, @@ -56,17 +57,28 @@ export default { executeGiraffeql(this, { getRepositoryLatestVersion: true, }).then((res) => { - this.latestVersion = res - if (this.latestVersion && this.currentVersion !== this.latestVersion) { + this.latestVersion = res.tagName + if (this.hasNewerVersion) { // only open the snackbar if not DEV if (this.currentVersion !== 'DEV') { - this.open = true + // only show the snackbar if it has been at least 3 minutes since the release. if it has been more than 3 mins, show immediately + setTimeout(() => { + this.snackbarStatus = true + this.showNewerVersionIcon = true + }, Math.min(3 * 60 * 1000, Math.max(0, 3 * 60 * 1000 - (new Date() - new Date(res.createdAt))))) + } else { + this.showNewerVersionIcon = true } - this.hasNewerVersion = true } }) }, + computed: { + hasNewerVersion() { + return this.latestVersion && this.currentVersion !== this.latestVersion + }, + }, + methods: { reloadPage() { location.reload() diff --git a/frontend/components/interface/crud/crudRecordInterface.vue b/frontend/components/interface/crud/crudRecordInterface.vue index c8c00c5..6dbb325 100644 --- a/frontend/components/interface/crud/crudRecordInterface.vue +++ b/frontend/components/interface/crud/crudRecordInterface.vue @@ -3,7 +3,7 @@ {{ icon || recordInfo.icon || 'mdi-domain' }} {{ - title || `${recordInfo.pluralName}` + title || recordInfo.title || recordInfo.pluralName }} diff --git a/frontend/components/navigation/navDrawer.vue b/frontend/components/navigation/navDrawer.vue index f1f6da0..4b232c9 100644 --- a/frontend/components/navigation/navDrawer.vue +++ b/frontend/components/navigation/navDrawer.vue @@ -1,11 +1,7 @@