npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bigbinary/neeto-google-drive-frontend

v2.0.15

Published

Reusable Google Drive integration for Neeto products.

Readme

neeto-google-drive-nano

Start with Quick start. It is the shortest path to a working, per-owner Google Drive connection.

This nano provides a reusable Rails engine and frontend package for connecting a Neeto product resource to Google Drive.

Examples of an owner and its exported resource:

| Product | Integration owner | Exported resource | | --------- | ----------------- | ----------------- | | neetoForm | Form | Submission | | neetoCal | Meeting | Booking |

Each owner gets its own Google account connection. Google Drive credentials are not shared with Google Sheets or with another owner.

What the nano provides

Backend engine

The Rails engine owns:

  1. Google OAuth with the drive.file and userinfo.email scopes.
  2. Encrypted access-token and refresh-token storage.
  3. Per-owner connection status, account details, settings, and main-folder ID.
  4. Google Drive file, folder, upload, query, and token-refresh operations.
  5. Connect, configure, disconnect, and OAuth callback API endpoints.

It also handles these edge cases:

  • It reuses an omitted refresh token only when reconnecting the same Google account.
  • It creates a new main folder when the stored folder was deleted or trashed.
  • It synchronizes a folder name changed directly in Google Drive.
  • It prevents concurrent requests from leaving duplicate main folders.
  • It disconnects invalid authorization without deleting existing Drive files.

Frontend package

The frontend package exports:

import {
  GoogleDriveIcon,
  GoogleDriveIntegration,
  GoogleDriveSettings,
  useGoogleDrive,
} from "@bigbinary/neeto-google-drive-frontend";

The connection UI includes:

  • Sign in with Google.
  • Connected-account email.
  • Copy and open main-folder link.
  • Configure and disconnect actions.
  • OAuth and authorization error messages.

The settings UI includes:

  • Main-folder name.
  • Per-resource folder toggle and naming-field builder.
  • Save, cancel, loading, and missing-integration states.
  • An optional renderAdditionalSettings slot for host-owned options.

Product-specific settings such as attachment-field selection or a PDF export toggle are not built into the nano. The host renders them through renderAdditionalSettings and stores their values in additional_settings.

What the host product must provide

The nano does not know how a form submission or calendar booking works. The host product owns:

  1. Tenant-scoped owner lookup and authorization.
  2. Product-specific naming fields and any extra settings UI.
  3. The background job that decides when to export.
  4. Resource formatting, attachment lookup, and PDF generation.
  5. Product routes, page layout, breadcrumbs, help links, and lifecycle cleanup.

There is no reconnect email in the nano. A host can add one, but it is not required for the integration to work.

Quick start

1. Install the backend engine

Add the gem:

source "NEETO_GEM_SERVER_URL" do
  gem "neeto-google-drive-engine"
end

Install it:

bundle install

Mount the engine in config/routes.rb:

Rails.application.routes.draw do
  mount NeetoGoogleDriveEngine::Engine, at: "/neeto_google_drive"
end

Copy and run the migration:

bundle exec rails neeto_google_drive_engine:install:migrations
bundle exec rails db:migrate

2. Configure Google OAuth

Create config/initializers/neeto_google_drive_engine.rb:

# frozen_string_literal: true

NeetoGoogleDriveEngine.application_name = "NeetoForm"
NeetoGoogleDriveEngine.oauth_app_url =
  Rails.application.vault.google_drive[:oauth_app_url]
NeetoGoogleDriveEngine.oauth_client_id =
  Rails.application.vault.google_drive[:client_id]
NeetoGoogleDriveEngine.oauth_client_secret =
  Rails.application.vault.google_drive[:client_secret]
NeetoGoogleDriveEngine.oauth_state_private_key =
  Rails.application.vault.jwt[:connect_private_key]
NeetoGoogleDriveEngine.oauth_state_public_key =
  Rails.application.vault.jwt[:connect_public_key]
NeetoGoogleDriveEngine.user_class = "User"

Register this complete redirect URI in the Google OAuth client:

<oauth_app_url>/neeto_google_drive/api/v1/oauth

For example:

https://connect.neetoform.net/neeto_google_drive/api/v1/oauth

Use a dedicated Google Drive OAuth configuration. Do not reuse a Google Sheets token or make Drive credentials shared across owners.

The OAuth state is signed with RS256 and expires after two hours. The callback rejects invalid, expired, or tenant-inaccessible owner data.

3. Add the owner contract

Include NeetoGoogleDriveEngine::HasGoogleDrive in the model that owns the connection:

class Form < ApplicationRecord
  include NeetoGoogleDriveEngine::HasGoogleDrive
  include GoogleDriveIntegrable
end

The host adapter can implement these methods:

module GoogleDriveIntegrable
  extend ActiveSupport::Concern

  def google_drive_default_folder_name
    name
  end

  def google_drive_default_resource_folder_name_fields
    []
  end

  def google_drive_default_additional_settings
    {}
  end

  def google_drive_naming_fields
    raise NotImplementedError
  end

  def google_drive_additional_settings_data
    {}
  end

  def google_drive_redirect_url(query)
    raise NotImplementedError
  end
end

Method responsibilities:

| Method | Required | Purpose | | -------------------------------------------------- | -------- | ---------------------------------------------------- | | google_drive_naming_fields | Yes | Fields available in the resource-folder name builder | | google_drive_redirect_url | Yes | Absolute product URL used after OAuth | | google_drive_default_folder_name | No | Initial main-folder name; defaults to name | | google_drive_default_resource_folder_name_fields | No | Initially selected naming-field IDs | | google_drive_default_additional_settings | No | Default host-owned settings hash; defaults to {} | | google_drive_additional_settings_data | No | Metadata for the host settings UI; defaults to {} |

A naming field has this shape:

{ id: "customer_name", label: "Customer name", kind: "text" }

Only id and label are required by the frontend.

Product-specific options (attachments, PDF, notes, and so on) belong in additional_settings. Use google_drive_additional_settings_data to send UI metadata such as available attachment fields to renderAdditionalSettings.

4. Scope and authorize the engine controllers

The engine intentionally leaves product authorization to the host.

Override the API controller:

# app/overrides/controllers/neeto_google_drive_engine/api/v1/base_controller_override.rb

NeetoGoogleDriveEngine::Api::V1::BaseController.class_eval do
  private

    def load_owner
      @owner = current_user.organization.forms.find(params[:owner_id])
    end

    def authorize_google_drive!
      authorize(:form_setting, :can_manage_integrations?)
    end
end

Configure OAuth owner lookup separately because the OAuth callback does not use the regular authenticated API session:

NeetoGoogleDriveEngine.oauth_owner_resolver = lambda do |user, owner_id|
  user.organization.forms.find(owner_id)
end

Never use an unscoped Form.find, Meeting.find, or other owner lookup in the controller override or OAuth resolver.

5. Install and render the frontend

Install the package:

yarn add @bigbinary/neeto-google-drive-frontend

Render the connection screen:

import { GoogleDriveIntegration } from "@bigbinary/neeto-google-drive-frontend";

const GoogleDriveConnection = ({ canManageIntegrations, formId }) => (
  <GoogleDriveIntegration
    canManageIntegrations={canManageIntegrations}
    helpUrl="https://help.neetoform.com/google-drive"
    managePath={`/admin/form/${formId}/settings/integrations/google-drive/manage`}
    ownerId={formId}
    resourceName="submission"
    resourceNamePlural="submissions"
  />
);

Render the settings screen:

import { GoogleDriveSettings } from "@bigbinary/neeto-google-drive-frontend";

const GoogleDriveManage = ({
  canManageIntegrations,
  formId,
  navigateToIntegration,
}) => (
  <GoogleDriveSettings
    canManageIntegrations={canManageIntegrations}
    ownerId={formId}
    resourceName="submission"
    renderAdditionalSettings={FormGoogleDriveAdditionalSettings}
    renderMissingIntegration={() => <MissingIntegration />}
    onCancel={navigateToIntegration}
    onSave={navigateToIntegration}
  />
);

GoogleDriveIntegration requires resourceName and resourceNamePlural. GoogleDriveSettings requires only resourceName. Pass "booking" / "bookings" to the integration and "booking" to the settings in neetoCal.

renderAdditionalSettings is optional. Pass it only when the product needs extra options. The component receives Formik props plus additionalSettingsData from the owner. Read and write host values under values.additionalSettings (for example saveResourcePdf or selectedAttachmentFieldIds).

Products that do not need extra options, such as a first neetoCal version, omit renderAdditionalSettings and get only the shared folder settings.

The host must load the nano's neetoGoogleDrive translations into its i18next resources.

Backend API

The mounted engine exposes:

| Method | Endpoint | Purpose | | -------- | --------------------------------------------------- | --------------------------------------------------------- | | GET | /neeto_google_drive/api/v1/google_drive/:owner_id | Connection state, authorization URL, settings, and fields | | PATCH | /neeto_google_drive/api/v1/google_drive/:owner_id | Update settings and rename the connected main folder | | DELETE | /neeto_google_drive/api/v1/google_drive/:owner_id | Disconnect without deleting Drive files | | GET | /neeto_google_drive/api/v1/oauth | Google OAuth callback |

The GET response contains:

{
  "authorizationUrl": "https://accounts.google.com/...",
  "connected": true,
  "connectedEmail": "[email protected]",
  "folderUrl": "https://drive.google.com/drive/folders/...",
  "error": "",
  "settings": {
    "folderName": "Customer feedback",
    "createResourceFolder": true,
    "resourceFolderNameFields": ["full_name", "submission_date"],
    "additionalSettings": {
      "selectedAttachmentFieldIds": ["resume"],
      "saveResourcePdf": true
    }
  },
  "namingFields": [],
  "additionalSettingsData": {
    "attachmentFields": [{ "id": "resume", "label": "Resume" }]
  }
}

additionalSettings and additionalSettingsData are host-owned. The nano persists and returns them without interpreting their keys.

The exact JSON key casing follows the host application's response transformation.

Data owned by the engine

NeetoGoogleDriveEngine::GoogleDrive stores:

  • Polymorphic owner.
  • Active or inactive status.
  • Connected Google account UID and email.
  • Main-folder ID and name.
  • Export settings and the latest authorization error.

NeetoGoogleDriveEngine::Credential stores:

  • Encrypted access token.
  • Encrypted refresh token.
  • Token expiry time.
  • One-to-one link to the integration.

google_drive.export_configuration returns a JSON-safe settings snapshot for a background job:

{
  "create_resource_folder" => true,
  "resource_folder_name_fields" => ["full_name", "submission_date"],
  "additional_settings" => {
    "selected_attachment_field_ids" => ["resume"],
    "save_resource_pdf" => true
  }
}

The host job reads product-specific keys from additional_settings. Pass this snapshot and the current credential ID to the job. The credential ID lets a stale queued job stop after the owner disconnects or changes accounts.

Using the Drive client from a host exporter

The host exporter can use the nano-owned client:

client = NeetoGoogleDriveEngine::Client.new(
  owner.google_drive.credential
)

Available operations:

client.get_file(file_id)
client.create_folder(
  name: folder_name,
  parent_id: parent_id,
  app_properties: metadata
)
client.rename_file(file_id, name)
client.delete_file(file_id)
client.list_files(
  parent_id: parent_id,
  mime_type: mime_type,
  app_properties: metadata
)
client.upload_file(
  name: filename,
  parent_id: parent_id,
  upload_source: file.path,
  content_type: content_type,
  app_properties: metadata
)

Pass structured filters to list_files:

files = client.list_files(
  parent_id: target_folder.id,
  mime_type: NeetoGoogleDriveEngine::Client::FOLDER_MIME_TYPE,
  app_properties: {
    "neeto_google_drive_resource" => "submission_folder",
    "neeto_form_submission_id" => submission.id
  }
)

Do not construct Google Drive query strings in the host. The client escapes values and owns Google-specific query syntax.

Use nano-owned metadata keys for data shared across products:

metadata = NeetoGoogleDriveEngine::FileMetadata

{
  metadata::OWNER_ID_PROPERTY => owner.id,
  metadata::OWNER_TYPE_PROPERTY => owner.class.name,
  metadata::RESOURCE_PROPERTY => "main_folder"
}

Keep product-specific keys, such as neeto_form_submission_id, in the host product.

neetoForm example: implemented

neetoForm uses Form as the connection owner and Submission as the exported resource.

Model adapter

module GoogleDriveIntegrable
  extend ActiveSupport::Concern

  SUBMISSION_DATE_FIELD = "submission_date"
  SUBMISSION_ID_FIELD = "submission_id"
  ATTACHMENT_RECORD_KINDS =
    %w[file_upload take_photo digital_signature].freeze

  def google_drive_default_folder_name
    title
  end

  def google_drive_default_resource_folder_name_fields
    naming_records = answer_records.where.not(kind: ATTACHMENT_RECORD_KINDS)
    default_record =
      naming_records.find_by(kind: :full_name) || naming_records.first

    [default_record&.id&.to_s, SUBMISSION_DATE_FIELD].compact
  end

  def google_drive_naming_fields
    answer_records
      .where.not(kind: ATTACHMENT_RECORD_KINDS)
      .map do |record|
        {
          id: record.id.to_s,
          label: record.formatted_label,
          kind: record.kind
        }
      end
      .concat(
        [
          {
            id: SUBMISSION_DATE_FIELD,
            label: "Submission date",
            kind: "system"
          },
          {
            id: SUBMISSION_ID_FIELD,
            label: "Submission ID",
            kind: "system"
          }
        ]
      )
  end

  def google_drive_default_additional_settings
    {
      selected_attachment_field_ids: google_drive_attachment_fields.pluck(:id),
      save_resource_pdf: true
    }
  end

  def google_drive_additional_settings_data
    { attachment_fields: google_drive_attachment_fields }
  end

  def google_drive_attachment_fields
    answer_records
      .where(kind: ATTACHMENT_RECORD_KINDS)
      .map { |record| { id: record.id.to_s, label: record.formatted_label } }
  end

  def google_drive_redirect_url(query)
    url = URI(google_drive_integration_page_url)
    url.query = query.to_query
    url.to_s
  end
end

Attachment fields and the PDF toggle are Form-owned. The nano only stores them inside additional_settings and returns attachment field metadata through google_drive_additional_settings_data for Form's renderAdditionalSettings UI.

Background export

neetoForm snapshots the credential and settings when the submission completes:

if form.google_drive_exportable?
  google_drive = form.google_drive

  Integrations::GoogleDrive::SubmissionExporterJob.perform_async(
    submission.id,
    google_drive.credential.id,
    google_drive.export_configuration
  )
end

The host job owns:

  • Submission completion checks.
  • ActiveStorage attachment lookup.
  • Submission-folder naming.
  • Locale-aware PDF generation.
  • Retry scheduling and product logging.

neetoCal example: planned

This is a proposed adapter based on neetoCal's current model and route conventions. It is not implemented in neetoCal yet.

Use Meeting as the connection owner and Booking as the exported resource. Do not attach the connection to each Booking.

Planned initializer

NeetoGoogleDriveEngine.application_name = "NeetoCal"
NeetoGoogleDriveEngine.oauth_app_url =
  Rails.application.vault.google_drive[:oauth_app_url]
NeetoGoogleDriveEngine.oauth_client_id =
  Rails.application.vault.google_drive[:client_id]
NeetoGoogleDriveEngine.oauth_client_secret =
  Rails.application.vault.google_drive[:client_secret]
NeetoGoogleDriveEngine.oauth_state_private_key =
  Rails.application.vault.jwt[:connect_private_key]
NeetoGoogleDriveEngine.oauth_state_public_key =
  Rails.application.vault.jwt[:connect_public_key]
NeetoGoogleDriveEngine.user_class = "User"

Planned model adapter

module Meetings::GoogleDriveConcern
  extend ActiveSupport::Concern

  BOOKER_NAME_FIELD = "booker_name"
  BOOKING_DATE_FIELD = "booking_date"
  BOOKING_ID_FIELD = "booking_id"

  included do
    include NeetoGoogleDriveEngine::HasGoogleDrive
  end

  def google_drive_default_folder_name
    name
  end

  def google_drive_default_resource_folder_name_fields
    [BOOKER_NAME_FIELD, BOOKING_DATE_FIELD]
  end

  def google_drive_naming_fields
    [
      { id: BOOKER_NAME_FIELD, label: "Booker name", kind: "system" },
      { id: BOOKING_DATE_FIELD, label: "Booking date", kind: "system" },
      { id: BOOKING_ID_FIELD, label: "Booking ID", kind: "system" }
    ]
  end

  def google_drive_redirect_url(query)
    url = URI(
      "#{organization.root_url}/admin/scheduling-links/" \
        "#{sid}/settings/integrations/google-drive"
    )
    url.query = query.to_query
    url.to_s
  end
end

Planned controller configuration

Use neetoCal's organization scoping and existing Google Sheet permission until a dedicated Google Drive permission is introduced:

NeetoGoogleDriveEngine::Api::V1::BaseController.class_eval do
  private

    def load_owner
      @owner = @organization.meetings.find(params[:owner_id])
    end

    def authorize_google_drive!
      authorize(Meeting, :can_manage_google_sheet?)
    end
end
NeetoGoogleDriveEngine.oauth_owner_resolver = lambda do |user, owner_id|
  user.organization.meetings.find(owner_id)
end

Planned frontend adapter

import {
  GoogleDriveIntegration,
  GoogleDriveSettings,
} from "@bigbinary/neeto-google-drive-frontend";

const BookingDriveConnection = ({
  canManageGoogleSheet,
  managePath,
  meetingId,
}) => (
  <GoogleDriveIntegration
    canManageIntegrations={canManageGoogleSheet}
    managePath={managePath}
    ownerId={meetingId}
    resourceName="booking"
    resourceNamePlural="bookings"
  />
);

const BookingDriveSettings = ({
  canManageGoogleSheet,
  meetingId,
  navigateToIntegration,
}) => (
  <GoogleDriveSettings
    canManageIntegrations={canManageGoogleSheet}
    ownerId={meetingId}
    resourceName="booking"
    onCancel={navigateToIntegration}
    onSave={navigateToIntegration}
  />
);

Omit renderAdditionalSettings until Cal needs product-specific options. Connection copy uses resourceName / resourceNamePlural; settings labels use resourceName.

When neetoCal adds booking files or a booking PDF, render those controls in a Cal-owned renderAdditionalSettings component, store their values in additional_settings, and implement the export logic in neetoCal.

Frontend props

GoogleDriveIntegration

| Prop | Required | Purpose | | ----------------------- | -------- | -------------------------------------------------- | | ownerId | Yes | ID of the model that owns the integration | | canManageIntegrations | Yes | Enables the integration query for authorized users | | managePath | Yes | Host route opened by Manage Google Drive | | resourceName | Yes | Singular UI noun | | resourceNamePlural | Yes | Plural UI noun | | helpUrl | No | Learn-more link |

GoogleDriveSettings

| Prop | Required | Purpose | | -------------------------- | -------- | ----------------------------------------------- | | ownerId | Yes | ID of the model that owns the integration | | canManageIntegrations | Yes | Enables the settings query for authorized users | | onCancel | Yes | Host navigation after Cancel | | onSave | No | Host navigation after a successful save | | renderMissingIntegration | No | Host UI when the owner is disconnected | | renderAdditionalSettings | No | Host component for product-specific options | | resourceName | Yes | Singular noun used in settings labels |

useGoogleDrive

Use the hook when the host only needs connection status:

const { isConnected, isLoading } = useGoogleDrive({
  ownerId: meetingId,
  canManageIntegrations: canManageGoogleSheet,
});

It also returns update, disconnect, loading, and disconnect-dialog state.

Connection and folder behavior

Connect

  1. The UI fetches a signed Google authorization URL.
  2. Google redirects to the engine OAuth callback.
  3. The callback verifies state, scopes, user, and tenant-scoped owner.
  4. The engine stores encrypted credentials for that owner.
  5. The engine creates or restores the owner's main Drive folder.

Reconnect

  • Reconnecting the same account preserves settings and the main-folder ID.
  • An omitted refresh token is reused only for the same Google account UID.
  • Connecting another account clears the old folder ID.
  • The new account receives its own main folder.

Disconnect

  • The encrypted credential is destroyed.
  • The integration becomes inactive.
  • Settings and folder metadata remain stored.
  • Existing Google Drive folders and files are not deleted.
  • No reconnect email is sent.

Error handling

The nano exposes reusable error categories:

NeetoGoogleDriveEngine::AUTHORIZATION_ERRORS
NeetoGoogleDriveEngine::TRANSIENT_ERRORS
NeetoGoogleDriveEngine::EXPORT_MAX_ATTEMPTS

A host job can use them like this:

def perform
  export_resource!
rescue *NeetoGoogleDriveEngine::TRANSIENT_ERRORS => exception
  raise TransientError.new(exception)
rescue *NeetoGoogleDriveEngine::AUTHORIZATION_ERRORS
  NeetoGoogleDriveEngine::DisconnectService.new(
    google_drive,
    error: "authorization_expired"
  ).process!
end

The nano stores the error for the connection UI. User notification channels remain host-owned.

Testing

Run the backend suite:

bundle exec rails test

Build the frontend package:

yarn build

The backend suite covers:

  • Integration and credential models.
  • OAuth state and token exchange.
  • Same-account and different-account reconnects.
  • Drive query escaping and token refresh.
  • Main-folder creation, recovery, rename, and concurrency behavior.

Host products should separately test their authorization overrides, export job, resource formatter, attachments, and PDFs.

Local frontend development

Follow the frontend package development guide to build and consume the package locally.

Publishing

Follow the building and releasing packages guide.