ngocnicholas / airtable.net

Airtable .NET API Client
MIT License
141 stars 34 forks source link

[docs] Uspert #85

Closed sommmen closed 7 months ago

sommmen commented 7 months ago

Hiya,

I had some trouble figuring out the right way to upsert. There is no way AFAIK to manually create an AirtableRecord or AirtableRecord<T> so i can use it with UpdateMultipleRecords(..., AirtableRecord[] records, ...),.

It seems you need to use the IdField class without setting an id. Perhaps we can add a sample to the docs?

Ex. method, has some bloat to support poco.

/// <summary>
/// Performs an update or insert
/// See: <see href="https://github.com/ngocnicholas/airtable.net/wiki/Documentation#replacemultiplerecords-method"/>
/// See: <see href="https://airtable.com/developers/web/api/update-multiple-records#upserts"/>
/// </summary>
/// <typeparam name="T">Table type</typeparam>
/// <param name="tableIdOrName">Airtable id or name</param>
/// <param name="entities">Entities to upsert</param>
/// <param name="keyExpression">Key to diff on</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<(int created, int updated)> Upsert<T>(string tableIdOrName, ICollection<T> entities,
    Expression<Func<T, object>> keyExpression, CancellationToken cancellationToken = default)
{
    var properties = typeof(T)
        .GetProperties(BindingFlags.Public | BindingFlags.Instance);

    var keyName = (keyExpression.Body as MemberExpression 
                   ?? ((UnaryExpression) keyExpression.Body).Operand as MemberExpression)?.Member.Name
                   ?? throw new ArgumentOutOfRangeException(nameof(keyExpression), "Cannot get the member name from the key expression.");

    // We use IdFields here without specifying an Id, as per the web api docs.
    var idFields = entities
        .Select(c => new IdFields
        {
            FieldsCollection = properties
                .Select(x => new { x.Name, Value = x.GetMethod!.Invoke(c, []) })
                .ToDictionary(x => x.Name, x => x.Value)
        })
        .ToArray();

    var created = 0;
    var updated = 0;

    const int maxUpdateSize = 10; // Airtable spec
    foreach (var batch in idFields.Chunk(maxUpdateSize))
    {
        cancellationToken.ThrowIfCancellationRequested();

        var response = await _airtableBase.UpdateMultipleRecords(tableIdOrName, idFields: batch, typecast: true, performUpsert: new PerformUpsert
        {
            FieldsToMergeOn = [keyName]
        });

        if (response.AirtableApiError != null)
            throw response.AirtableApiError;

        created = response.CreatedRecords.Length;
        updated = response.UpdatedRecords.Length;
    }

    return (created, updated);
}
ngocnicholas commented 7 months ago

When you need AirtableRecord[] to use with upsert and you need to obtain an AirtableRecord or AirtableRecord, use ListRecords or ListRecords and then tailor them to what you need.

When upserting is enabled, the id property of records becomes optional. Records that do not include id will use the fields chosen by fieldsToMergeOn as an external ID to match with existing records. I updated the doc to clarify it.