lib/fbe/if_absent.rb



# frozen_string_literal: true

# SPDX-FileCopyrightText: Copyright (c) 2024-2025 Zerocracy
# SPDX-License-Identifier: MIT

require 'others'
require 'time'
require_relative '../fbe'
require_relative 'fb'

# Injects a fact if it's absent in the factbase, otherwise returns nil.
#
# Checks if a fact with the same property values already exists. If not,
# creates a new fact. System properties (_id, _time, _version) are excluded
# from the uniqueness check.
#
# Here is what you do when you want to add a fact to the factbase, but
# don't want to make a duplicate of an existing one:
#
#  require 'fbe/if_absent'
#  n = Fbe.if_absent do |f|
#    f.what = 'something'
#    f.details = 'important'
#  end
#  return if n.nil?  # Fact already existed
#  n.when = Time.now # Add additional properties to the new fact
#
# This code will definitely create one fact with +what+ equals to +something+
# and +details+ equals to +important+, while the +when+ will be equal to the
# time of its first creation.
#
# @param [Factbase] fb The factbase to check and insert into (defaults to Fbe.fb)
# @yield [Factbase::Fact] A proxy fact object to set properties on
# @return [nil, Factbase::Fact] nil if fact exists, otherwise the newly created fact
# @note String values are properly escaped in queries
# @note Time values are converted to UTC ISO8601 format for comparison
# @example Ensure unique user registration
#   user = Fbe.if_absent do |f|
#     f.type = 'user'
#     f.email = 'john@example.com'
#   end
#   if user
#     user.registered_at = Time.now
#     puts "New user created"
#   else
#     puts "User already exists"
#   end
def Fbe.if_absent(fb: Fbe.fb)
  attrs = {}
  f =
    others(map: attrs) do |*args|
      k = args[0]
      if k.end_with?('=')
        k = k[0..-2].to_sym
        v = args[1]
        raise "Can't set #{k} to nil" if v.nil?
        raise "Can't set #{k} to empty string" if v.is_a?(String) && v.empty?
        @map[k] = v
      else
        @map[k.to_sym]
      end
    end
  yield f
  q = attrs.except('_id', '_time', '_version').map do |k, v|
    vv = v.to_s
    if v.is_a?(String)
      vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
    elsif v.is_a?(Time)
      vv = v.utc.iso8601
    end
    "(eq #{k} #{vv})"
  end.join(' ')
  q = "(and #{q})"
  before = fb.query(q).each.to_a.first
  return nil if before
  n = fb.insert
  attrs.each { |k, v| n.send(:"#{k}=", v) }
  n
end